/* * Copyright © 2014-2015 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.dataset.DatasetAdmin; import co.cask.cdap.api.dataset.table.Get; import co.cask.cdap.api.dataset.table.Row; import co.cask.cdap.api.dataset.table.Scan; import co.cask.cdap.api.dataset.table.Scanner; import co.cask.cdap.api.dataset.table.Table; import co.cask.cdap.data2.dataset2.TableAssert; import co.cask.tephra.Transaction; import co.cask.tephra.TransactionAware; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.junit.Assert; import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.NavigableMap; /** * unit-test * @param <T> table type */ public abstract class BufferingTableTest<T extends BufferingTable> extends TableConcurrentTest<T> { @Test public void testRollingBackAfterExceptionDuringPersist() throws Exception { DatasetAdmin admin = getTableAdmin(CONTEXT1, MY_TABLE); admin.create(); try { Transaction tx1 = txClient.startShort(); BufferingTable myTable1 = new BufferingTableWithPersistingFailure(getTable(CONTEXT1, MY_TABLE)); myTable1.startTx(tx1); // write some data but not commit myTable1.put(R1, a(C1), a(V1)); myTable1.put(R2, a(C2), a(V2)); // verify can see changes inside tx TableAssert.assertRow(a(C1, V1), myTable1.get(R1, a(C1))); TableAssert.assertRow(a(C2, V2), myTable1.get(R2, a(C2))); // persisting changes try { // should simulate exception myTable1.commitTx(); Assert.assertFalse(true); } catch (Throwable th) { // Expected simulated exception } // let's pretend that after persisting changes we still got conflict when finalizing tx, so // rolling back changes Assert.assertTrue(myTable1.rollbackTx()); // making tx visible txClient.abort(tx1); // start new tx Transaction tx2 = txClient.startShort(); Table myTable2 = getTable(CONTEXT1, MY_TABLE); ((TransactionAware) myTable2).startTx(tx2); // verify don't see rolled back changes TableAssert.assertRow(a(), myTable2.get(R1, a(C1))); TableAssert.assertRow(a(), myTable2.get(R2, a(C2))); } finally { admin.drop(); } } /** * Tests that writes being buffered in memory by the client are still visible during scans. */ @Test public void testScanWithBuffering() throws Exception { String testScanWithBuffering = "testScanWithBuffering"; DatasetAdmin admin = getTableAdmin(CONTEXT1, testScanWithBuffering); admin.create(); try { // Transaction tx1 = txClient.startShort(); Table table1 = getTable(CONTEXT1, testScanWithBuffering); ((TransactionAware) table1).startTx(tx1); table1.put(Bytes.toBytes("1_01"), a(C1), a(V1)); table1.put(Bytes.toBytes("1_02"), a(C1), a(V1)); table1.put(Bytes.toBytes("1_03"), a(C1), a(V1)); // written values should not yet be persisted TableAssert.assertScan(new byte[0][], new byte[0][][], ((BufferingTable) table1).scanPersisted( new Scan(Bytes.toBytes("1_"), Bytes.toBytes("2_")))); // buffered values should be visible in a scan TableAssert.assertScan(a(Bytes.toBytes("1_01"), Bytes.toBytes("1_02"), Bytes.toBytes("1_03")), aa(a(C1, V1), a(C1, V1), a(C1, V1)), table1.scan(Bytes.toBytes("1_"), Bytes.toBytes("2_"))); Assert.assertTrue(txClient.canCommit(tx1, ((TransactionAware) table1).getTxChanges())); Assert.assertTrue(((TransactionAware) table1).commitTx()); Assert.assertTrue(txClient.commit(tx1)); Transaction tx2 = txClient.startShort(); ((TransactionAware) table1).startTx(tx2); // written values should be visible after commit TableAssert.assertScan(a(Bytes.toBytes("1_01"), Bytes.toBytes("1_02"), Bytes.toBytes("1_03")), aa(a(C1, V1), a(C1, V1), a(C1, V1)), table1.scan(Bytes.toBytes("1_"), Bytes.toBytes("2_"))); txClient.commit(tx2); Transaction tx3 = txClient.startShort(); ((TransactionAware) table1).startTx(tx3); // test merging of buffered writes on existing rows table1.put(Bytes.toBytes("1_01"), a(C2), a(V2)); table1.put(Bytes.toBytes("1_02"), a(C1), a(V2)); table1.put(Bytes.toBytes("1_02a"), a(C1), a(V1)); table1.put(Bytes.toBytes("1_02b"), a(C1), a(V1)); table1.put(Bytes.toBytes("1_04"), a(C2), a(V2)); // persisted values should be the same TableAssert.assertScan(a(Bytes.toBytes("1_01"), Bytes.toBytes("1_02"), Bytes.toBytes("1_03")), aa(a(C1, V1), a(C1, V1), a(C1, V1)), ((BufferingTable) table1).scanPersisted( new Scan(Bytes.toBytes("1_"), Bytes.toBytes("2_")))); // all values should be visible in buffered scan TableAssert.assertScan(a(Bytes.toBytes("1_01"), Bytes.toBytes("1_02"), Bytes.toBytes("1_02a"), Bytes.toBytes("1_02b"), Bytes.toBytes("1_03"), Bytes.toBytes("1_04")), aa(a(C1, V1, C2, V2), // 1_01 a(C1, V2), // 1_02 a(C1, V1), // 1_02a a(C1, V1), // 1_02b a(C1, V1), // 1_03 a(C2, V2)), // 1_04 table1.scan(Bytes.toBytes("1_"), Bytes.toBytes("2_"))); Assert.assertTrue(txClient.canCommit(tx3, ((TransactionAware) table1).getTxChanges())); Assert.assertTrue(((TransactionAware) table1).commitTx()); txClient.commit(tx3); Transaction tx4 = txClient.startShort(); ((TransactionAware) table1).startTx(tx4); // all values should be visible after commit TableAssert.assertScan(a(Bytes.toBytes("1_01"), Bytes.toBytes("1_02"), Bytes.toBytes("1_02a"), Bytes.toBytes("1_02b"), Bytes.toBytes("1_03"), Bytes.toBytes("1_04")), aa(a(C1, V1, C2, V2), // 1_01 a(C1, V2), // 1_02 a(C1, V1), // 1_02a a(C1, V1), // 1_02b a(C1, V1), // 1_03 a(C2, V2)), // 1_04 table1.scan(Bytes.toBytes("1_"), Bytes.toBytes("2_"))); txClient.commit(tx4); } finally { admin.drop(); } } @Test public void testChangingParamsAndReturnValues() throws Exception { // The test verifies that one can re-use byte arrays passed as parameters to write methods of a table without // affecting the stored data. // Also, one can re-use (modify) returned data from the table without affecting the stored data. DatasetAdmin admin = getTableAdmin(CONTEXT1, MY_TABLE); admin.create(); try { // writing some data: we'll need it to test delete later Transaction tx = txClient.startShort(); BufferingTable table = getTable(CONTEXT1, MY_TABLE); table.startTx(tx); table.put(new byte[] {0}, new byte[] {9}, new byte[] {8}); table.commitTx(); txClient.commit(tx); // start new for in-mem buffer behavior testing tx = txClient.startShort(); table.startTx(tx); // write some data but not commit byte[] rowParam = new byte[] {1}; byte[] colParam = new byte[] {2}; byte[] valParam = Bytes.toBytes(3L); table.put(rowParam, colParam, valParam); verify123(table); // change passed earlier byte arrays in place, this should not affect stored previously values rowParam[0]++; colParam[0]++; valParam[0]++; verify123(table); // try get row and change returned values in place, which should not affect the data stored Row getRow = table.get(new byte[] {1}); Map<byte[], byte[]> getRowResult = getRow.getColumns(); Assert.assertEquals(1, getRowResult.size()); byte[] colFromGetRow = getRowResult.keySet().iterator().next(); byte[] valFromGetRow = getRowResult.get(colFromGetRow); getRowResult.remove(new byte[] {2}); Assert.assertArrayEquals(new byte[] {2}, colFromGetRow); Assert.assertArrayEquals(Bytes.toBytes(3L), valFromGetRow); colFromGetRow[0]++; valFromGetRow[0]++; verify123(table); // try get set of columns in a row and change returned values in place, which should not affect the data stored Row getColumnSetRow = table.get(new byte[] {1}); Map<byte[], byte[]> getColumnSetResult = getColumnSetRow.getColumns(); Assert.assertEquals(1, getColumnSetResult.size()); byte[] colFromGetColumnSet = getColumnSetResult.keySet().iterator().next(); byte[] valFromGetColumnSet = getColumnSetResult.values().iterator().next(); getColumnSetResult.remove(new byte[] {2}); Assert.assertArrayEquals(new byte[] {2}, colFromGetColumnSet); Assert.assertArrayEquals(Bytes.toBytes(3L), valFromGetColumnSet); colFromGetColumnSet[0]++; valFromGetColumnSet[0]++; verify123(table); // try get column and change returned value in place, which should not affect the data stored byte[] valFromGetColumn = table.get(new byte[] {1}, new byte[] {2}); Assert.assertArrayEquals(Bytes.toBytes(3L), valFromGetColumn); valFromGetColumn[0]++; verify123(table); // try scan and change returned value in place, which should not affect the data stored Scanner scan = table.scan(new byte[] {1}, null); Row next = scan.next(); Assert.assertNotNull(next); byte[] rowFromScan = next.getRow(); Assert.assertArrayEquals(new byte[] {1}, rowFromScan); Map<byte[], byte[]> cols = next.getColumns(); Assert.assertEquals(1, cols.size()); byte[] colFromScan = cols.keySet().iterator().next(); Assert.assertArrayEquals(new byte[] {2}, colFromScan); byte[] valFromScan = next.get(new byte[] {2}); Assert.assertNotNull(valFromScan); Assert.assertArrayEquals(Bytes.toBytes(3L), valFromScan); Assert.assertNull(scan.next()); cols.remove(new byte[] {2}); rowFromScan[0]++; colFromScan[0]++; valFromScan[0]++; verify123(table); // try delete and change params in place: this should not affect stored data rowParam = new byte[] {1}; colParam = new byte[] {2}; table.delete(rowParam, colParam); Assert.assertNull(table.get(new byte[] {1}, new byte[] {2})); Assert.assertArrayEquals(new byte[] {8}, table.get(new byte[] {0}, new byte[] {9})); rowParam[0] = 0; colParam[0] = 9; Assert.assertNull(table.get(new byte[] {1}, new byte[] {2})); Assert.assertArrayEquals(new byte[] {8}, table.get(new byte[] {0}, new byte[] {9})); // try increment column and change params in place: this should not affect stored data byte[] rowIncParam = new byte[] {1}; byte[] colIncParam = new byte[] {2}; table.increment(rowIncParam, colIncParam, 3); verify123(table); rowIncParam[0]++; colIncParam[0]++; verify123(table); // try increment set of columns and change params in place, try also to change values in returned map: this all // should not affect stored data. rowIncParam = new byte[] {1}; colIncParam = new byte[] {2}; table.increment(rowIncParam, colIncParam, -1); table.increment(rowIncParam, new byte[][] {colIncParam}, new long[] {1}); verify123(table); rowIncParam[0]++; colIncParam[0]++; verify123(table); // try increment and change returned values: should not affect the stored data rowIncParam = new byte[] {1}; colIncParam = new byte[] {2}; table.increment(rowIncParam, colIncParam, -1); Row countersRow = table.incrementAndGet(rowIncParam, new byte[][] {colIncParam}, new long[] {1}); Map<byte[], byte[]> counters = countersRow.getColumns(); Assert.assertEquals(1, counters.size()); byte[] colFromInc = counters.keySet().iterator().next(); Assert.assertArrayEquals(new byte[] {2}, colFromInc); Assert.assertEquals(3, Bytes.toLong(counters.get(colFromInc))); counters.remove(new byte[] {2}); colFromInc[0]++; verify123(table); // try increment write and change params in place: this should not affect stored data rowIncParam = new byte[] {1}; colIncParam = new byte[] {2}; table.increment(rowIncParam, colIncParam, -1); table.increment(rowIncParam, new byte[][] {colIncParam}, new long[] {1}); verify123(table); rowIncParam[0]++; colIncParam[0]++; verify123(table); // try compareAndSwap and change params in place: this should not affect stored data byte[] rowSwapParam = new byte[] {1}; byte[] colSwapParam = new byte[] {2}; byte[] valSwapParam = Bytes.toBytes(3L); table.compareAndSwap(rowSwapParam, colSwapParam, Bytes.toBytes(3L), Bytes.toBytes(4L)); table.compareAndSwap(rowSwapParam, colSwapParam, Bytes.toBytes(4L), valSwapParam); verify123(table); rowSwapParam[0]++; colSwapParam[0]++; valSwapParam[0]++; verify123(table); // We don't care to persist changes and commit tx here: we tested what we wanted } finally { admin.drop(); } } // This test is in Buffering table because it needs to test the transaction change prefix @Test public void testTxChangePrefix() throws Exception { String tableName = "same"; DatasetAdmin admin1 = getTableAdmin(CONTEXT1, tableName); DatasetAdmin admin2 = getTableAdmin(CONTEXT2, tableName); admin1.create(); admin2.create(); try { BufferingTable table1 = getTable(CONTEXT1, tableName); BufferingTable table2 = getTable(CONTEXT2, tableName); // write some values in table1 Transaction tx1 = txClient.startShort(); table1.startTx(tx1); table1.put(R1, a(C1), a(V1)); Collection<byte []> tx1Changes = table1.getTxChanges(); Assert.assertTrue(txClient.canCommit(tx1, tx1Changes)); Assert.assertTrue(table1.commitTx()); Assert.assertTrue(txClient.commit(tx1)); table1.postTxCommit(); // write some values in table2 Transaction tx2 = txClient.startShort(); table2.startTx(tx2); table2.put(R1, a(C1), a(V1)); Collection<byte []> tx2Changes = table2.getTxChanges(); Assert.assertTrue(txClient.canCommit(tx2, tx2Changes)); Assert.assertTrue(table2.commitTx()); Assert.assertTrue(txClient.commit(tx2)); table1.postTxCommit(); String tx1ChangePrefix = new String(table1.getNameAsTxChangePrefix()); String tx2ChangePrefix = new String(table2.getNameAsTxChangePrefix()); String tx1Change = new String(((ArrayList<byte []>) tx1Changes).get(0)); String tx2Change = new String(((ArrayList<byte []>) tx2Changes).get(0)); Assert.assertNotEquals(tx1ChangePrefix, tx2ChangePrefix); Assert.assertTrue(tx1ChangePrefix.contains(NAMESPACE1.getId())); Assert.assertTrue(tx2ChangePrefix.contains(NAMESPACE2.getId())); Assert.assertTrue(tx1Change.startsWith(tx1ChangePrefix)); Assert.assertTrue(tx2Change.startsWith(tx2ChangePrefix)); } finally { admin1.drop(); admin2.drop(); } } @Test public void testMultiGetIncludesBuffer() throws Exception { DatasetAdmin admin = getTableAdmin(CONTEXT1, MY_TABLE); admin.create(); try { // persist some data BufferingTable table = getTable(CONTEXT1, MY_TABLE); Transaction tx1 = txClient.startShort(); table.startTx(tx1); // writing a couple rows // table should look like the following, with everything in the buffer // c1 c2 c3 c4 // r1 1 2 3 - // r2 - 3 2 1 table.put(R1, a(C1, C2, C3), lb(1, 2, 3)); table.put(R2, a(C2, C3, C4), lb(3, 2, 1)); // check that multi-get can see buffered writes List<Row> rows = table.get(Lists.newArrayList(new Get(R1), new Get(R2))); Assert.assertEquals(2, rows.size()); TableAssert.assertRow(rows.get(0), R1, a(C1, C2, C3), lb(1, 2, 3)); TableAssert.assertRow(rows.get(1), R2, a(C2, C3, C4), lb(3, 2, 1)); // check multi-get with gets that specify columns, and one get that should return an empty row rows = table.get(Lists.newArrayList(new Get(R1, C2, C3), new Get(R2, C2, C3), new Get(R3))); Assert.assertEquals(3, rows.size()); TableAssert.assertRow(rows.get(0), R1, a(C2, C3), lb(2, 3)); TableAssert.assertRow(rows.get(1), R2, a(C2, C3), lb(3, 2)); Assert.assertTrue(rows.get(2).isEmpty()); // persist changes Collection<byte []> txChanges = table.getTxChanges(); Assert.assertTrue(txClient.canCommit(tx1, txChanges)); Assert.assertTrue(table.commitTx()); Assert.assertTrue(txClient.commit(tx1)); table.postTxCommit(); // start another transaction Transaction tx2 = txClient.startShort(); table.startTx(tx2); // now add another row, delete a row, and change some column values // table should look like the following // c1 c2 c3 c4 c5 // r1 - - 3 2 - // r3 - - - - 1 table.put(R1, a(C2, C3, C4), lb(4, 3, 2)); table.delete(R1, a(C1, C2)); table.delete(R2); table.put(R3, C5, L1); // verify multi-get sees persisted data with buffer applied on top rows = table.get(Lists.newArrayList(new Get(R1), new Get(R2), new Get(R3))); Assert.assertEquals(3, rows.size()); TableAssert.assertRow(rows.get(0), R1, a(C3, C4), lb(3, 2)); Assert.assertTrue(rows.get(1).isEmpty()); TableAssert.assertRow(rows.get(2), R3, a(C5), lb(1)); // pretend there was a write conflict and rollback changes Assert.assertTrue(table.rollbackTx()); txClient.abort(tx2); // start another transaction and make sure it can't see what was done before Transaction tx3 = txClient.startShort(); table.startTx(tx3); rows = table.get(Lists.newArrayList(new Get(R1), new Get(R2))); Assert.assertEquals(2, rows.size()); TableAssert.assertRow(rows.get(0), R1, a(C1, C2, C3), lb(1, 2, 3)); TableAssert.assertRow(rows.get(1), R2, a(C2, C3, C4), lb(3, 2, 1)); } finally { admin.drop(); } } private void verify123(BufferingTable table) throws Exception { byte[] row = new byte[] {1}; byte[] col = new byte[] {2}; byte[] val = Bytes.toBytes((long) 3); verify(table, row, col, val); row[0]++; col[0]++; val[0]++; Assert.assertNull(table.get(row, col)); Assert.assertTrue(table.get(row).isEmpty()); Assert.assertArrayEquals(new byte[] {8}, table.get(new byte[] {0}, new byte[] {9})); } private void verify(BufferingTable table, byte[] row, byte[] col, byte[] val) throws Exception { // get column Assert.assertArrayEquals(val, table.get(row, col)); // get set of columns Row getColSetRow = table.get(row, new byte[][] {col}); Map<byte[], byte[]> getColSetResult = getColSetRow.getColumns(); Assert.assertEquals(1, getColSetResult.size()); Assert.assertArrayEquals(val, getColSetResult.get(col)); // get row Row getRow = table.get(row); Map<byte[], byte[]> getRowResult = getRow.getColumns(); Assert.assertEquals(1, getRowResult.size()); Assert.assertArrayEquals(val, getRowResult.get(col)); // scan Scanner scan = table.scan(row, null); Row next = scan.next(); Assert.assertNotNull(next); Assert.assertArrayEquals(row, next.getRow()); Assert.assertArrayEquals(val, next.get(col)); Assert.assertNull(scan.next()); } // This class looks weird, this is what we have to do to override persist method to make it throw exception in the // middle. NOTE: We want to test how every implementation of BufferingTable handles undoing changes in this // case, otherwise we would just test the method of BufferingTable directly. public static class BufferingTableWithPersistingFailure extends BufferingTable { private BufferingTable delegate; public BufferingTableWithPersistingFailure(BufferingTable delegate) { super(delegate.getTableName()); this.delegate = delegate; } // override persist to simulate failure in the middle @Override protected void persist(NavigableMap<byte[], NavigableMap<byte[], Update>> buff) throws Exception { // persists only first change and throws exception NavigableMap<byte[], NavigableMap<byte[], Update>> toPersist = Maps.newTreeMap(Bytes.BYTES_COMPARATOR); if (buff.size() > 0) { toPersist.put(buff.firstEntry().getKey(), buff.firstEntry().getValue()); } delegate.persist(toPersist); throw new RuntimeException("Simulating failure in the middle of persist"); } // implementing abstract methods @Override protected void undo(NavigableMap<byte[], NavigableMap<byte[], Update>> persisted) throws Exception { delegate.undo(persisted); } @Override protected NavigableMap<byte[], byte[]> getPersisted(byte[] row, byte[][] columns) throws Exception { return delegate.getPersisted(row, columns); } @Override protected NavigableMap<byte[], byte[]> getPersisted(byte[] row, byte[] startColumn, byte[] stopColumn, int limit) throws Exception { return delegate.getPersisted(row, startColumn, stopColumn, limit); } @Override protected Scanner scanPersisted(Scan scan) throws Exception { return delegate.scanPersisted(scan); } // propagating tx to delegate @Override public void startTx(Transaction tx) { super.startTx(tx); delegate.startTx(tx); } } }