/*
* Copyright © 2014 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.api.dataset.lib;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.dataset.DatasetProperties;
import co.cask.cdap.data2.dataset2.DatasetFrameworkTestUtil;
import co.cask.cdap.proto.Id;
import co.cask.tephra.TransactionExecutor;
import co.cask.tephra.TransactionFailureException;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Time series table tests.
*/
public class TimeseriesTableTest {
@ClassRule
public static DatasetFrameworkTestUtil dsFrameworkUtil = new DatasetFrameworkTestUtil();
private static final Id.DatasetInstance metricsTableInstance =
Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "metricsTable");
private static TimeseriesTable table;
@BeforeClass
public static void beforeClass() throws Exception {
dsFrameworkUtil.createInstance("timeseriesTable", metricsTableInstance, DatasetProperties.EMPTY);
table = dsFrameworkUtil.getInstance(metricsTableInstance);
}
@AfterClass
public static void afterClass() throws Exception {
dsFrameworkUtil.deleteInstance(metricsTableInstance);
}
@Test
public void testDataSet() throws Exception {
TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(table);
// this test runs all operations synchronously
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
byte[] metric1 = Bytes.toBytes("metric1");
byte[] metric2 = Bytes.toBytes("metric2");
byte[] tag1 = Bytes.toBytes("111");
byte[] tag2 = Bytes.toBytes("22");
byte[] tag3 = Bytes.toBytes("3");
byte[] tag4 = Bytes.toBytes("123");
long hour = TimeUnit.HOURS.toMillis(1);
long second = TimeUnit.SECONDS.toMillis(1);
long ts = System.currentTimeMillis();
// m1e1 = metric: 1, entity: 1
TimeseriesTable.Entry m1e1 = new TimeseriesTable.Entry(metric1, Bytes.toBytes(3L), ts, tag3, tag2, tag1);
table.write(m1e1);
TimeseriesTable.Entry m1e2 = new TimeseriesTable.Entry(metric1, Bytes.toBytes(10L), ts + 2 * second, tag3);
table.write(m1e2);
TimeseriesTable.Entry m1e3 = new TimeseriesTable.Entry(metric1, Bytes.toBytes(15L), ts + 2 * hour, tag1);
table.write(m1e3);
TimeseriesTable.Entry m1e4 = new TimeseriesTable.Entry(metric1, Bytes.toBytes(23L), ts + 3 * hour, tag2, tag3);
table.write(m1e4);
TimeseriesTable.Entry m1e5 = new TimeseriesTable.Entry(metric1, Bytes.toBytes(55L), ts + 3 * hour + 2 * second);
table.write(m1e5);
TimeseriesTable.Entry m2e1 = new TimeseriesTable.Entry(metric2, Bytes.toBytes(4L), ts);
table.write(m2e1);
TimeseriesTable.Entry m2e2 = new TimeseriesTable.Entry(metric2, Bytes.toBytes(11L), ts + 2 * second, tag2);
table.write(m2e2);
TimeseriesTable.Entry m2e3 = new TimeseriesTable.Entry(metric2, Bytes.toBytes(16L), ts + 2 * hour, tag2);
table.write(m2e3);
TimeseriesTable.Entry m2e4 = new TimeseriesTable.Entry(metric2, Bytes.toBytes(24L), ts + 3 * hour, tag1, tag3);
table.write(m2e4);
TimeseriesTable.Entry m2e5 = new TimeseriesTable.Entry(metric2, Bytes.toBytes(56L), ts + 3 * hour + 2 * second,
tag3, tag1);
table.write(m2e5);
// whole interval is searched
assertReadResult(table.read(metric1, ts, ts + 5 * hour), m1e1, m1e2, m1e3, m1e4, m1e5);
assertReadResult(table.read(metric1, ts, ts + 5 * hour, tag2), m1e1, m1e4);
assertReadResult(table.read(metric1, ts, ts + 5 * hour, tag4));
assertReadResult(table.read(metric1, ts, ts + 5 * hour, tag2, tag4));
// This is extreme case, should not be really used by anyone. Still we want to test that it won't fail.
// It returns nothing because there's hard limit on the number of rows traversed during the read.
assertReadResult(table.read(metric1, 0, Long.MAX_VALUE));
// test pagination read
assertReadResult(table.read(metric1, ts, ts + 5 * hour, 1, 2), m1e2, m1e3);
// part of the interval
assertReadResult(table.read(metric1, ts + second, ts + 2 * second), m1e2);
assertReadResult(table.read(metric1, ts + hour, ts + 3 * hour), m1e3, m1e4);
assertReadResult(table.read(metric1, ts + second, ts + 3 * hour), m1e2, m1e3, m1e4);
assertReadResult(table.read(metric1, ts + second, ts + 3 * hour, tag3), m1e2, m1e4);
assertReadResult(table.read(metric1, ts + second, ts + 3 * hour, tag3, tag2), m1e4);
// different metric
assertReadResult(table.read(metric2, ts + hour, ts + 3 * hour, tag2), m2e3);
}
});
}
@Test(expected = TransactionFailureException.class)
public void testInvalidTimeRangeCondition() throws Exception {
TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(table);
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
long ts = System.currentTimeMillis();
table.read(Bytes.toBytes("any"), ts, ts - 100);
}
});
}
@Test
public void testValidTimeRangesAreAllowed() throws Exception {
TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(table);
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
long ts = System.currentTimeMillis();
Iterator<TimeseriesTable.Entry> temp = table.read(Bytes.toBytes("any"), ts, ts);
Assert.assertFalse(temp.hasNext());
temp = table.read(Bytes.toBytes("any"), ts, ts + 100);
Assert.assertFalse(temp.hasNext());
}
});
}
// TODO: test for wrong params: end time less than start time
private void assertReadResult(Iterator<TimeseriesTable.Entry> resultIterator, TimeseriesTable.Entry... entries) {
int count = 0;
while (resultIterator.hasNext()) {
assertEquals(entries[count], resultIterator.next());
count++;
}
Assert.assertEquals(entries.length, count);
}
private void assertEquals(final TimeseriesTable.Entry left, final TimeseriesTable.Entry right) {
Assert.assertArrayEquals(left.getKey(), right.getKey());
Assert.assertEquals(left.getTimestamp(), right.getTimestamp());
Assert.assertArrayEquals(left.getValue(), right.getValue());
assertEqualsIgnoreOrder(left.getTags(), right.getTags());
}
private void assertEqualsIgnoreOrder(final byte[][] left, final byte[][] right) {
Arrays.sort(left, Bytes.BYTES_COMPARATOR);
Arrays.sort(right, Bytes.BYTES_COMPARATOR);
Assert.assertEquals(left.length, right.length);
for (int i = 0; i < left.length; i++) {
Assert.assertArrayEquals(left[i], right[i]);
}
}
/**
* Time series table format test.
*/
@Test
public void testColumnNameFormat() {
// Note: tags are sorted lexographically
byte[] tag1 = Bytes.toBytes("111");
byte[] tag2 = Bytes.toBytes("22");
byte[] tag3 = Bytes.toBytes("3");
long ts = System.currentTimeMillis();
byte[][] tags = { tag1, tag2, tag3 };
byte[] columnName = TimeseriesTable.createColumnName(ts, tags);
Assert.assertEquals(ts, TimeseriesTable.parseTimeStamp(columnName));
Assert.assertTrue(TimeseriesTable.hasTags(columnName));
}
@Test
public void testColumnNameFormatWithNoTags() {
long ts = System.currentTimeMillis();
byte[] columnName = TimeseriesTable.createColumnName(ts, new byte[0][]);
Assert.assertEquals(ts, TimeseriesTable.parseTimeStamp(columnName));
Assert.assertFalse(TimeseriesTable.hasTags(columnName));
}
@Test
public void testRowFormat() {
// 1 min
long timeIntervalPerRow = TimeUnit.MINUTES.toMillis(1);
byte[] key = Bytes.toBytes("key");
long ts1 = timeIntervalPerRow + 1;
// These are the things we care about:
// * If the timestamps fall into same time interval then row keys are same
Assert.assertArrayEquals(TimeseriesTable.createRow(key, ts1, timeIntervalPerRow),
TimeseriesTable.createRow(key, ts1 + timeIntervalPerRow / 2, timeIntervalPerRow));
// * If the timestamps don't fall into same time interval then row keys are different
Assert.assertFalse(Arrays.equals(TimeseriesTable.createRow(key, ts1, timeIntervalPerRow),
TimeseriesTable.createRow(key, ts1 + timeIntervalPerRow * 2, timeIntervalPerRow)));
// * If timestamp A > timestamp B then row key A > row key B (we will have some optimization logic based on that)
Assert.assertTrue(
Bytes.compareTo(TimeseriesTable.createRow(key, ts1, timeIntervalPerRow),
TimeseriesTable.createRow(key, ts1 + timeIntervalPerRow * 5, timeIntervalPerRow)) < 0);
// * For different keys the constructed rows are different
byte[] key2 = Bytes.toBytes("KEY2");
Assert.assertFalse(Arrays.equals(TimeseriesTable.createRow(key, ts1, timeIntervalPerRow),
TimeseriesTable.createRow(key2, ts1, timeIntervalPerRow)));
// * We can get all possible rows for a given time interval with getRowsForInterval
List<byte[]> rows = new ArrayList<>();
long startTime = timeIntervalPerRow * 4;
long endTime = timeIntervalPerRow * 100;
for (long i = startTime; i <= endTime; i += timeIntervalPerRow / 10) {
byte[] row = TimeseriesTable.createRow(key, i, timeIntervalPerRow);
if (rows.isEmpty() || !Arrays.equals(rows.get(rows.size() - 1), row)) {
rows.add(row);
}
}
int intervalsCount = (int) TimeseriesTable.getTimeIntervalsCount(startTime, endTime, timeIntervalPerRow);
byte[][] rowsForInterval = new byte[intervalsCount][];
for (int i = 0; i < intervalsCount; i++) {
rowsForInterval[i] = TimeseriesTable.getRowOfKthInterval(key, startTime, i, timeIntervalPerRow);
}
assertEquals(rows.toArray(new byte[rows.size()][]), rowsForInterval);
// same requirement, one more test (covers some bug I fixed)
Assert.assertEquals(2, TimeseriesTable.getTimeIntervalsCount(timeIntervalPerRow + 1, timeIntervalPerRow * 2 + 1,
timeIntervalPerRow));
}
private void assertEquals(final byte[][] left, final byte[][] right) {
Assert.assertEquals(left.length, right.length);
for (int i = 0; i < left.length; i++) {
Assert.assertArrayEquals(left[i], right[i]);
}
}
}