/* * 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.data.batch.Split; import co.cask.cdap.api.data.batch.SplitReader; 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 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.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Random; import java.util.SortedSet; /** * Key value table test. */ public class KeyValueTableTest { @ClassRule public static DatasetFrameworkTestUtil dsFrameworkUtil = new DatasetFrameworkTestUtil(); static final byte[] KEY1 = Bytes.toBytes("KEY1"); static final byte[] KEY2 = Bytes.toBytes("KEY2"); static final byte[] KEY3 = Bytes.toBytes("KEY3"); static final byte[] VAL1 = Bytes.toBytes("VAL1"); static final byte[] VAL2 = Bytes.toBytes("VAL2"); static final byte[] VAL3 = Bytes.toBytes("VAL3"); private static final Id.DatasetInstance testInstance = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "test"); private static KeyValueTable kvTable; @BeforeClass public static void beforeClass() throws Exception { dsFrameworkUtil.createInstance("keyValueTable", testInstance, DatasetProperties.EMPTY); kvTable = dsFrameworkUtil.getInstance(testInstance); } @AfterClass public static void afterClass() throws Exception { dsFrameworkUtil.deleteInstance(testInstance); } @Test public void testSyncWriteReadSwapDelete() throws Exception { TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(kvTable); // this test runs all operations synchronously txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // write a value and read it back kvTable.write(KEY1, VAL1); Assert.assertArrayEquals(VAL1, kvTable.read(KEY1)); // update the value and read it back kvTable.write(KEY1, VAL2); Assert.assertArrayEquals(VAL2, kvTable.read(KEY1)); // attempt to swap, expecting old value Assert.assertFalse(kvTable.compareAndSwap(KEY1, VAL1, VAL3)); Assert.assertArrayEquals(VAL2, kvTable.read(KEY1)); // swap the value and read it back Assert.assertTrue(kvTable.compareAndSwap(KEY1, VAL2, VAL3)); Assert.assertArrayEquals(VAL3, kvTable.read(KEY1)); // delete the value and verify its gone kvTable.delete(KEY1); Assert.assertNull(kvTable.read(KEY1)); } }); } @Test public void testASyncWriteReadSwapDelete() throws Exception { TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(kvTable); // defer all writes until commit txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // write a value kvTable.write(KEY2, VAL1); } }); // verify synchronously txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // verify that the value is now visible Assert.assertArrayEquals(VAL1, kvTable.read(KEY2)); } }); // defer all writes until commit txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // update the value kvTable.write(KEY2, VAL2); } }); // verify synchronously txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // verify that the value is now visible Assert.assertArrayEquals(VAL2, kvTable.read(KEY2)); } }); // defer all writes until commit txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // write a swap, this should fail Assert.assertFalse(kvTable.compareAndSwap(KEY2, VAL1, VAL3)); Assert.assertArrayEquals(VAL2, kvTable.read(KEY2)); } }); // defer all writes until commit txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // swap the value Assert.assertTrue(kvTable.compareAndSwap(KEY2, VAL2, VAL3)); } }); // verify synchronously txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // verify the value was swapped Assert.assertArrayEquals(VAL3, kvTable.read(KEY2)); } }); // defer all writes until commit txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // delete the value kvTable.delete(KEY2); } }); // verify synchronously txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // verify it is gone now Assert.assertNull(kvTable.read(KEY2)); } }); } @Test public void testTransactionAcrossTables() throws Exception { Id.DatasetInstance t1 = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "t1"); Id.DatasetInstance t2 = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "t2"); dsFrameworkUtil.createInstance("keyValueTable", t1, DatasetProperties.EMPTY); dsFrameworkUtil.createInstance("keyValueTable", t2, DatasetProperties.EMPTY); final KeyValueTable table1 = dsFrameworkUtil.getInstance(t1); final KeyValueTable table2 = dsFrameworkUtil.getInstance(t2); TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(table1, table2); // write a value to table1 and verify it txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { table1.write(KEY1, VAL1); Assert.assertArrayEquals(VAL1, table1.read(KEY1)); table2.write(KEY2, VAL2); Assert.assertArrayEquals(VAL2, table2.read(KEY2)); } }); // start a new transaction try { txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // add a write for table 1 to the transaction table1.write(KEY1, VAL2); // submit a delete for table 2 table2.delete(KEY2); throw new RuntimeException("Cancel transaction"); } }); Assert.fail("Transaction should have been cancelled"); } catch (TransactionFailureException e) { Assert.assertEquals("Cancel transaction", e.getCause().getMessage()); } // add a swap for a third table that should fail Assert.assertFalse(kvTable.compareAndSwap(KEY3, VAL1, VAL1)); txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { Assert.assertArrayEquals(VAL1, table1.read(KEY1)); Assert.assertArrayEquals(VAL2, table2.read(KEY2)); } }); // verify synchronously that old value are still there final KeyValueTable table1v2 = dsFrameworkUtil.getInstance(t1); final KeyValueTable table2v2 = dsFrameworkUtil.getInstance(t2); TransactionExecutor txnlv2 = dsFrameworkUtil.newTransactionExecutor(table1v2, table2v2); txnlv2.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { Assert.assertArrayEquals(VAL1, table1v2.read(KEY1)); Assert.assertArrayEquals(VAL2, table2v2.read(KEY2)); } }); dsFrameworkUtil.deleteInstance(t1); dsFrameworkUtil.deleteInstance(t2); } @Test public void testScanning() throws Exception { Id.DatasetInstance tScan = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "tScan"); dsFrameworkUtil.createInstance("keyValueTable", tScan, DatasetProperties.EMPTY); final KeyValueTable t = dsFrameworkUtil.getInstance(tScan); TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(t); // start a transaction txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // write 0..1000 to the table for (int i = 0; i < 1000; i++) { byte[] key = Bytes.toBytes(i); t.write(key, key); } } }); // start a transaction, verify scan txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // scan with start row '0' and end row '1000' and make sure we have 1000 records Iterator<KeyValue<byte[], byte[]>> keyValueIterator = t.scan(Bytes.toBytes(0), Bytes.toBytes(1000)); int rowCount = 0; while (keyValueIterator.hasNext()) { rowCount++; keyValueIterator.next(); } Assert.assertEquals(1000, rowCount); } }); // 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 { // scan with start row '0' and end row '1000' and make sure we have 1000 records CloseableIterator<KeyValue<byte[], byte[]>> keyValueIterator = t.scan(Bytes.toBytes(0), Bytes.toBytes(200)); int rowCount = 0; while (keyValueIterator.hasNext() && (rowCount < 100)) { rowCount++; keyValueIterator.next(); } keyValueIterator.close(); try { keyValueIterator.next(); Assert.fail("Reading after closing Scanner returned result."); } catch (NoSuchElementException e) { } } }); dsFrameworkUtil.deleteInstance(tScan); } @Test public void testBatchReads() throws Exception { Id.DatasetInstance tBatch = Id.DatasetInstance.from(DatasetFrameworkTestUtil.NAMESPACE_ID, "tBatch"); dsFrameworkUtil.createInstance("keyValueTable", tBatch, DatasetProperties.EMPTY); final KeyValueTable t = dsFrameworkUtil.getInstance(tBatch); TransactionExecutor txnl = dsFrameworkUtil.newTransactionExecutor(t); final SortedSet<Long> keysWritten = Sets.newTreeSet(); // start a transaction txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // write 1000 random values to the table and remember them in a set Random rand = new Random(451); for (int i = 0; i < 1000; i++) { long keyLong = rand.nextLong(); byte[] key = Bytes.toBytes(keyLong); t.write(key, key); keysWritten.add(keyLong); } } }); // start a sync transaction 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); } }); // start a sync transaction txnl.execute(new TransactionExecutor.Subroutine() { @Override public void apply() throws Exception { // get specific number of splits for a subrange SortedSet<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); } }); dsFrameworkUtil.deleteInstance(tBatch); } // helper to verify that the split readers for the given splits return exactly a set of keys private void verifySplits(KeyValueTable 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[], byte[]> reader = t.createSplitReader(split); reader.initialize(split); while (reader.nextKeyValue()) { byte[] key = reader.getCurrentKey(); byte[] value = reader.getCurrentValue(); // verify each row has the two columns written Assert.assertArrayEquals(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()); } }