/* * 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 com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; /** * Defines a class to test EntryScanner in TimeseriesTable */ public class TimeseriesTableScannerTest { @ClassRule public static DatasetFrameworkTestUtil dsFrameworkUtil = new DatasetFrameworkTestUtil(); private static final Id.DatasetInstance facts = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "facts"); private static final byte[] ALL_KEY = Bytes.toBytes("a"); private static final String SRC_TAG = "src"; private static final String DST_TAG = "dst"; private static final String SRC_DEVICE_ID_TAG = "src_device_id"; private static final String DEST_DEVICE_ID_TAG = "dest_device_id"; private static TimeseriesTable table = null; @BeforeClass public static void setup() throws Exception { dsFrameworkUtil.createInstance("timeseriesTable", facts, DatasetProperties.EMPTY); table = dsFrameworkUtil.getInstance(facts); } @AfterClass public static void tearDown() throws Exception { dsFrameworkUtil.deleteInstance(facts); } @Test public void test() throws Exception { long now = System.currentTimeMillis(); sendData(now); // verify testScan(now); testNoRow(now); testFilter(now); testNoTagMatch(now); testReadEntryWithoutTag(now); } private void sendData(long now) throws IOException, InterruptedException { // send facts in time window (now - 10 mins, now) Fact f1 = new Fact(now - TimeUnit.MINUTES.toMillis(1), ImmutableMap.of(SRC_TAG, "10.192.18.0", DST_TAG, "10.123.8.20", SRC_DEVICE_ID_TAG, "device1", DEST_DEVICE_ID_TAG, "device2")); writeFact(f1); Fact f2 = new Fact(now - TimeUnit.MINUTES.toMillis(2), ImmutableMap.of(SRC_TAG, "10.192.18.0", DST_TAG, "10.123.8.30", SRC_DEVICE_ID_TAG, "device1", DEST_DEVICE_ID_TAG, "device2")); writeFact(f2); Fact f3 = new Fact(now - TimeUnit.MINUTES.toMillis(10), ImmutableMap.of(SRC_TAG, "20.192.18.0", DST_TAG, "10.123.8.40", SRC_DEVICE_ID_TAG, "device1", DEST_DEVICE_ID_TAG, "device3")); writeFact(f3); // send facts in time window (now - 20 mins, now - 10 mins) Fact f4 = new Fact(now - TimeUnit.MINUTES.toMillis(12), ImmutableMap.of(SRC_TAG, "12.192.18.0", DST_TAG, "20.123.8.50", SRC_DEVICE_ID_TAG, "device2", DEST_DEVICE_ID_TAG, "device4")); writeFact(f4); Fact f5 = new Fact(now - TimeUnit.MINUTES.toMillis(15), ImmutableMap.of(SRC_TAG, "12.192.18.0", DST_TAG, "20.123.8.60", SRC_DEVICE_ID_TAG, "device4", DEST_DEVICE_ID_TAG, "device2")); writeFact(f5); // f6 and f7 are written to the table without tag. Fact f6 = new Fact(now - TimeUnit.SECONDS.toMillis(10), ImmutableMap.of(SRC_TAG, "1.1.1.1", DST_TAG, "20.123.8.60", SRC_DEVICE_ID_TAG, "device4", DEST_DEVICE_ID_TAG, "device2")); writeFact(f6); Fact f7 = new Fact(now - TimeUnit.SECONDS.toMillis(20), ImmutableMap.of(SRC_TAG, "1.1.1.1", DST_TAG, "20.123.8.60", SRC_DEVICE_ID_TAG, "device4", DEST_DEVICE_ID_TAG, "device2")); writeFact(f7); } /** * Tests the correctness of facts fetched by EntryScanner. */ private void testScan(long now) { long endTs = now; long startTs = now - TimeUnit.MINUTES.toMillis(18) + 1; // read facts in time window (now - 18mins, now) Iterator<TimeseriesTable.Entry> entryIterator = table.read(ALL_KEY, startTs, endTs); // dsts of the facts sent from the earliest ts to now ImmutableSet<String> expectedDsts = ImmutableSet.of("20.123.8.60", "20.123.8.50", "10.123.8.40", "10.123.8.30", "10.123.8.20"); Set<String> actualDsts = Sets.newHashSet(); while (entryIterator.hasNext()) { TimeseriesTable.Entry entry = entryIterator.next(); actualDsts.add(Bytes.toString(entry.getValue())); } Assert.assertEquals(5, expectedDsts.size()); Assert.assertEquals("dst is not correct", expectedDsts, actualDsts); } /** * Tests no entry returned in the time interval. */ private void testNoRow(long now) { long endTs = now + TimeUnit.MINUTES.toMillis(20); long startTs = now + TimeUnit.MINUTES.toMillis(10); Iterator<TimeseriesTable.Entry> entryIterator = table.read(ALL_KEY, startTs, endTs); Assert.assertFalse("No entry returned in the time interval", entryIterator.hasNext()); } /** * Tests iterator filter. queryTags will be sorted inside EntryScanner. */ private void testFilter(long now) { long endTs = now; long startTs = now - TimeUnit.MINUTES.toMicros(37); String[] queryTags = {"device2", "device1"}; Iterator<TimeseriesTable.Entry> entryIterator = table.read(ALL_KEY, startTs, endTs, Bytes.toByteArrays(queryTags)); List<TimeseriesTable.Entry> result = Lists.newArrayList(); while (entryIterator.hasNext()) { TimeseriesTable.Entry entry = entryIterator.next(); result.add(entry); } Assert.assertEquals(2, result.size()); } /** * Tests queryTags are not matched. */ private void testNoTagMatch(long now) { long endTs = now; long startTs = now - TimeUnit.MINUTES.toMicros(37); String[] queryTags = {"device5", "device2"}; Iterator<TimeseriesTable.Entry> entryIterator = table.read(ALL_KEY, startTs, endTs, Bytes.toByteArrays(queryTags)); Assert.assertFalse(entryIterator.hasNext()); } /** * Tests reading entries without tag. */ private void testReadEntryWithoutTag(long now) { long endTs = now; long startTs = now - TimeUnit.SECONDS.toMillis(30); List<TimeseriesTable.Entry> result = Lists.newArrayList(); // f6 and f7 are read back, since they are written to the table without tag. Iterator<TimeseriesTable.Entry> entryIterator = table.read(ALL_KEY, startTs, endTs); while (entryIterator.hasNext()) { result.add(entryIterator.next()); } Assert.assertEquals(2, result.size()); } private void writeFact(Fact fact) { long ts = fact.getTs(); byte[] srcTag = Bytes.toBytes(fact.getDimensions().get(SRC_DEVICE_ID_TAG)); byte[] destTag = Bytes.toBytes(fact.getDimensions().get(DEST_DEVICE_ID_TAG)); if (fact.getDimensions().get(SRC_DEVICE_ID_TAG).equals("1.1.1.1")) { table.write(new TimeseriesTable.Entry(ALL_KEY, Bytes.toBytes(fact.getDimensions().get(DST_TAG)), ts)); } else { table.write(new TimeseriesTable.Entry(ALL_KEY, Bytes.toBytes(fact.getDimensions().get(DST_TAG)), ts, srcTag, destTag)); } } private class Fact { // map dimension name -> value // NOTE: using concrete sorted map implementation here to ease json serde in tests private TreeMap<String, String> dimensions; private long ts; public Fact(long ts, Map<String, String> dimensions) { // todo: avoid copying maps this.dimensions = Maps.newTreeMap(ImmutableSortedMap.copyOf(dimensions)); this.ts = ts; } public Map<String, String> getDimensions() { return dimensions; } public long getTs() { return ts; } // this is handy method for re-using same fact instance public void setTs(long ts) { this.ts = ts; } public byte[] buildKey() { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> dimVal : dimensions.entrySet()) { sb.append(dimVal.getKey().length()).append(dimVal.getKey()); sb.append(dimVal.getValue().length()).append(dimVal.getValue()); } return Bytes.toBytes(sb.toString()); } @Override public String toString() { return Objects.toStringHelper(Fact.class) .add("ts", ts) .add("dimensions", dimensions) .toString(); } } }