/*
* 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.data2.dataset2.lib.table;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.data.batch.Split;
import co.cask.cdap.api.data.batch.SplitReader;
import co.cask.cdap.api.data.schema.Schema;
import co.cask.cdap.api.dataset.DatasetProperties;
import co.cask.cdap.api.dataset.lib.CloseableIterator;
import co.cask.cdap.api.dataset.lib.IntegerStore;
import co.cask.cdap.api.dataset.lib.IntegerStoreModule;
import co.cask.cdap.api.dataset.lib.KeyValue;
import co.cask.cdap.api.dataset.lib.KeyValueTable;
import co.cask.cdap.api.dataset.lib.ObjectStore;
import co.cask.cdap.api.dataset.lib.ObjectStores;
import co.cask.cdap.common.utils.ImmutablePair;
import co.cask.cdap.data2.dataset2.DatasetFrameworkTestUtil;
import co.cask.cdap.internal.io.ReflectionSchemaGenerator;
import co.cask.cdap.internal.io.TypeRepresentation;
import co.cask.cdap.proto.Id;
import co.cask.tephra.TransactionExecutor;
import co.cask.tephra.TransactionFailureException;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
/**
* Test for {@link co.cask.cdap.data2.dataset2.lib.table.ObjectStoreDataset}.
*/
public class ObjectStoreDatasetTest {
@ClassRule
public static DatasetFrameworkTestUtil dsFrameworkUtil = new DatasetFrameworkTestUtil();
private static final byte[] a = { 'a' };
private static final Id.DatasetModule integerStore =
Id.DatasetModule.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "integerStore");
@BeforeClass
public static void beforeClass() throws Exception {
dsFrameworkUtil.addModule(integerStore, new IntegerStoreModule());
}
@AfterClass
public static void afterClass() throws Exception {
dsFrameworkUtil.deleteModule(integerStore);
}
private void addIntegerStoreInstance(Id.DatasetInstance datasetInstanceId) throws Exception {
dsFrameworkUtil.createInstance("integerStore", datasetInstanceId, DatasetProperties.EMPTY);
}
@Test
public void testStringStore() throws Exception {
Id.DatasetInstance strings = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "strings");
createObjectStoreInstance(strings, String.class);
ObjectStoreDataset<String> stringStore = dsFrameworkUtil.getInstance(strings);
String string = "this is a string";
stringStore.write(a, string);
String result = stringStore.read(a);
Assert.assertEquals(string, result);
deleteAndVerify(stringStore, a);
dsFrameworkUtil.deleteInstance(strings);
}
@Test
public void testPairStore() throws Exception {
Id.DatasetInstance pairs = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "pairs");
createObjectStoreInstance(pairs, new TypeToken<ImmutablePair<Integer, String>>() { }.getType());
ObjectStoreDataset<ImmutablePair<Integer, String>> pairStore = dsFrameworkUtil.getInstance(pairs);
ImmutablePair<Integer, String> pair = new ImmutablePair<>(1, "second");
pairStore.write(a, pair);
ImmutablePair<Integer, String> result = pairStore.read(a);
Assert.assertEquals(pair, result);
deleteAndVerify(pairStore, a);
dsFrameworkUtil.deleteInstance(pairs);
}
@Test
public void testCustomStore() throws Exception {
Id.DatasetInstance customs = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "customs");
createObjectStoreInstance(customs, new TypeToken<Custom>() { }.getType());
ObjectStoreDataset<Custom> customStore = dsFrameworkUtil.getInstance(customs);
Custom custom = new Custom(42, Lists.newArrayList("one", "two"));
customStore.write(a, custom);
Custom result = customStore.read(a);
Assert.assertEquals(custom, result);
custom = new Custom(-1, null);
customStore.write(a, custom);
result = customStore.read(a);
Assert.assertEquals(custom, result);
deleteAndVerify(customStore, a);
dsFrameworkUtil.deleteInstance(customs);
}
@Test
public void testInnerStore() throws Exception {
Id.DatasetInstance inners = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "inners");
createObjectStoreInstance(inners, new TypeToken<CustomWithInner.Inner<Integer>>() { }.getType());
ObjectStoreDataset<CustomWithInner.Inner<Integer>> innerStore = dsFrameworkUtil.getInstance(inners);
CustomWithInner.Inner<Integer> inner = new CustomWithInner.Inner<>(42, new Integer(99));
innerStore.write(a, inner);
CustomWithInner.Inner<Integer> result = innerStore.read(a);
Assert.assertEquals(inner, result);
deleteAndVerify(innerStore, a);
dsFrameworkUtil.deleteInstance(inners);
}
@Test
public void testInstantiateWrongClass() throws Exception {
Id.DatasetInstance pairs = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "pairs");
createObjectStoreInstance(pairs, new TypeToken<ImmutablePair<Integer, String>>() { }.getType());
// note: due to type erasure, this succeeds
final ObjectStoreDataset<Custom> store = dsFrameworkUtil.getInstance(pairs);
TransactionExecutor storeTxnl = dsFrameworkUtil.newTransactionExecutor(store);
// but now it must fail with incompatible type
try {
storeTxnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
Custom custom = new Custom(42, Lists.newArrayList("one", "two"));
store.write(a, custom);
}
});
Assert.fail("write should have failed with incompatible type");
} catch (TransactionFailureException e) {
// expected
}
// write a correct object to the pair store
final ObjectStoreDataset<ImmutablePair<Integer, String>> pairStore = dsFrameworkUtil.getInstance(pairs);
TransactionExecutor pairStoreTxnl = dsFrameworkUtil.newTransactionExecutor(store);
final ImmutablePair<Integer, String> pair = new ImmutablePair<>(1, "second");
pairStoreTxnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
pairStore.write(a, pair); // should succeed
}
});
pairStoreTxnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
ImmutablePair<Integer, String> actualPair = pairStore.read(a);
Assert.assertEquals(pair, actualPair);
}
});
// now try to read that as a custom object, should fail with class cast
try {
storeTxnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
Custom custom = store.read(a);
Preconditions.checkNotNull(custom);
}
});
Assert.fail("write should have failed with class cast exception");
} catch (TransactionFailureException e) {
// expected
}
deleteAndVerify(pairStore, a);
dsFrameworkUtil.deleteInstance(pairs);
}
@Test
public void testWithCustomClassLoader() throws Exception {
Id.DatasetInstance kv = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "kv");
// create a dummy class loader that records the name of the class it loaded
final AtomicReference<String> lastClassLoaded = new AtomicReference<>(null);
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
lastClassLoaded.set(name);
return super.loadClass(name);
}
};
dsFrameworkUtil.createInstance("keyValueTable", kv, DatasetProperties.EMPTY);
KeyValueTable kvTable = dsFrameworkUtil.getInstance(kv);
Type type = Custom.class;
TypeRepresentation typeRep = new TypeRepresentation(type);
Schema schema = new ReflectionSchemaGenerator().generate(type);
ObjectStoreDataset<Custom> objectStore = new ObjectStoreDataset<>("kv", kvTable, typeRep, schema, loader);
// need to call this to actually load the Custom class, because the Custom class is no longer used in the
// ObjectStoreDataset's constructor, but rather lazily when its actually needed.
objectStore.getRecordType();
objectStore.write("dummy", new Custom(382, Lists.newArrayList("blah")));
// verify the class name was recorded (the dummy class loader was used).
Assert.assertEquals(Custom.class.getName(), lastClassLoaded.get());
deleteAndVerify(objectStore, Bytes.toBytes("dummy"));
dsFrameworkUtil.deleteInstance(kv);
}
@Test
public void testBatchCustomList() throws Exception {
Id.DatasetInstance customlist = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "customlist");
createObjectStoreInstance(customlist, new TypeToken<List<Custom>>() { }.getType());
final ObjectStoreDataset<List<Custom>> customStore = dsFrameworkUtil.getInstance(customlist);
TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(customStore);
final SortedSet<Long> keysWritten = Sets.newTreeSet();
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
List<Custom> customList1 = Arrays.asList(new Custom(1, Lists.newArrayList("one", "ONE")),
new Custom(2, Lists.newArrayList("two", "TWO")));
Random rand = new Random(100);
long key1 = rand.nextLong();
keysWritten.add(key1);
customStore.write(Bytes.toBytes(key1), customList1);
List<Custom> customList2 = Arrays.asList(new Custom(3, Lists.newArrayList("three", "THREE")),
new Custom(4, Lists.newArrayList("four", "FOUR")));
long key2 = rand.nextLong();
keysWritten.add(key2);
customStore.write(Bytes.toBytes(key2), customList2);
}
});
final SortedSet<Long> keysWrittenCopy = ImmutableSortedSet.copyOf(keysWritten);
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
// get the splits for the table
List<Split> splits = customStore.getSplits();
for (Split split : splits) {
SplitReader<byte[], List<Custom>> reader = customStore.createSplitReader(split);
reader.initialize(split);
while (reader.nextKeyValue()) {
byte[] key = reader.getCurrentKey();
Assert.assertTrue(keysWritten.remove(Bytes.toLong(key)));
}
}
// verify all keys have been read
if (!keysWritten.isEmpty()) {
System.out.println("Remaining [" + keysWritten.size() + "]: " + keysWritten);
}
Assert.assertTrue(keysWritten.isEmpty());
}
});
deleteAndVerifyInBatch(customStore, txnl, keysWrittenCopy);
dsFrameworkUtil.deleteInstance(customlist);
}
@Test
public void testBatchReads() throws Exception {
Id.DatasetInstance batch = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "batch");
createObjectStoreInstance(batch, String.class);
final ObjectStoreDataset<String> t = dsFrameworkUtil.getInstance(batch);
TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(t);
final SortedSet<Long> keysWritten = Sets.newTreeSet();
// write 1000 random values to the table and remember them in a set
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
Random rand = new Random(451);
for (int i = 0; i < 1000; i++) {
long keyLong = rand.nextLong();
byte[] key = Bytes.toBytes(keyLong);
t.write(key, Long.toString(keyLong));
keysWritten.add(keyLong);
}
}
});
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
// get the splits for the table
List<Split> splits = t.getSplits();
// read each split and verify the keys
SortedSet<Long> keysToVerify = Sets.newTreeSet(keysWritten);
verifySplits(t, splits, keysToVerify);
}
});
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
// get specific number of splits for a subrange
TreeSet<Long> keysToVerify = Sets.newTreeSet(keysWritten.subSet(0x10000000L, 0x40000000L));
List<Split> splits = t.getSplits(5, Bytes.toBytes(0x10000000L), Bytes.toBytes(0x40000000L));
Assert.assertTrue(splits.size() <= 5);
// read each split and verify the keys
verifySplits(t, splits, keysToVerify);
}
});
deleteAndVerifyInBatch(t, txnl, keysWritten);
dsFrameworkUtil.deleteInstance(batch);
}
@Test
public void testScanObjectStore() throws Exception {
Id.DatasetInstance scan = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "scan");
createObjectStoreInstance(scan, String.class);
final ObjectStoreDataset<String> t = dsFrameworkUtil.getInstance(scan);
TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(t);
// write 10 values
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
for (int i = 0; i < 10; i++) {
byte[] key = Bytes.toBytes(i);
t.write(key, String.valueOf(i));
}
}
});
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
Iterator<KeyValue<byte[], String>> objectsIterator = t.scan(Bytes.toBytes(0), Bytes.toBytes(10));
int sum = 0;
while (objectsIterator.hasNext()) {
sum += Integer.parseInt(objectsIterator.next().getValue());
}
//checking the sum equals sum of values from (0..9) which are the rows written and scanned for.
Assert.assertEquals(45, sum);
}
});
// start a transaction, scan part of them elements using scanner, close the scanner,
// then call next() on scanner, it should fail
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
CloseableIterator<KeyValue<byte[], String>> objectsIterator = t.scan(Bytes.toBytes(0), Bytes.toBytes(10));
int rowCount = 0;
while (objectsIterator.hasNext() && (rowCount < 5)) {
rowCount++;
}
objectsIterator.close();
try {
objectsIterator.next();
Assert.fail("Reading after closing Scanner returned result.");
} catch (NoSuchElementException e) {
}
}
});
dsFrameworkUtil.deleteInstance(scan);
}
// helper to verify that the split readers for the given splits return exactly a set of keys
private void verifySplits(ObjectStoreDataset<String> t, List<Split> splits, SortedSet<Long> keysToVerify)
throws InterruptedException {
// read each split and verify the keys, remove all read keys from the set
for (Split split : splits) {
SplitReader<byte[], String> reader = t.createSplitReader(split);
reader.initialize(split);
while (reader.nextKeyValue()) {
byte[] key = reader.getCurrentKey();
String value = reader.getCurrentValue();
// verify each row has the two columns written
Assert.assertEquals(Long.toString(Bytes.toLong(key)), value);
Assert.assertTrue(keysToVerify.remove(Bytes.toLong(key)));
}
}
// verify all keys have been read
if (!keysToVerify.isEmpty()) {
System.out.println("Remaining [" + keysToVerify.size() + "]: " + keysToVerify);
}
Assert.assertTrue(keysToVerify.isEmpty());
}
@Test
public void testSubclass() throws Exception {
Id.DatasetInstance intsInstance = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "ints");
addIntegerStoreInstance(intsInstance);
IntegerStore ints = dsFrameworkUtil.getInstance(intsInstance);
ints.write(42, 101);
Assert.assertEquals((Integer) 101, ints.read(42));
// test delete
ints.delete(42);
Assert.assertNull(ints.read(42));
dsFrameworkUtil.deleteInstance(intsInstance);
}
private void createObjectStoreInstance(Id.DatasetInstance datasetInstanceId, Type type) throws Exception {
dsFrameworkUtil.createInstance("objectStore", datasetInstanceId,
ObjectStores.objectStoreProperties(type, DatasetProperties.EMPTY));
}
private void deleteAndVerify(ObjectStore store, byte[] key) {
store.delete(key);
Assert.assertNull(store.read(key));
}
private void deleteAndVerifyInBatch(final ObjectStoreDataset t, TransactionExecutor txnl,
final SortedSet<Long> keysWritten) throws TransactionFailureException,
InterruptedException {
// delete all the keys written earlier
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
for (Long curKey : keysWritten) {
t.delete(Bytes.toBytes(curKey));
}
}
});
// verify that all the keys are deleted
txnl.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
for (Long curKey : keysWritten) {
Assert.assertNull(t.read(Bytes.toBytes(curKey)));
}
}
});
}
}