/* * Copyright © 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.increment.hbase; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.data.hbase.HBaseTestBase; import co.cask.cdap.data.hbase.HBaseTestFactory; import co.cask.cdap.data2.dataset2.lib.table.hbase.HBaseTable; import co.cask.cdap.data2.util.TableId; import co.cask.cdap.data2.util.hbase.HBaseTableUtil; import co.cask.cdap.data2.util.hbase.HBaseTableUtilFactory; import co.cask.cdap.proto.Id; import co.cask.tephra.TxConstants; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.client.Delete; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.util.Bytes; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * Common test cases for HBase version-specific {@code IncrementHandlerTest} implementations. */ public abstract class AbstractIncrementHandlerTest { private static final Logger LOG = LoggerFactory.getLogger(AbstractIncrementHandlerTest.class); @ClassRule public static final HBaseTestBase TEST_HBASE = new HBaseTestFactory().get(); protected static final byte[] EMPTY_BYTES = new byte[0]; protected static final byte[] FAMILY = Bytes.toBytes("i"); protected static Configuration conf; protected static CConfiguration cConf; protected static HBaseTableUtil tableUtil; protected long ts = 1; @BeforeClass public static void setup() throws Exception { conf = TEST_HBASE.getConfiguration(); cConf = CConfiguration.create(); tableUtil = new HBaseTableUtilFactory(cConf).get(); } @Test public void testIncrements() throws Exception { TableId tableId = TableId.from(Id.Namespace.DEFAULT, "incrementTest"); createTable(tableId); try (HTable table = new HBaseTableUtilFactory(cConf).get().createHTable(conf, tableId)) { byte[] colA = Bytes.toBytes("a"); byte[] row1 = Bytes.toBytes("row1"); // test column containing only increments table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); assertColumn(table, row1, colA, 3); // test intermixed increments and puts table.put(tableUtil.buildPut(row1).add(FAMILY, colA, ts++, Bytes.toBytes(5L)).build()); assertColumn(table, row1, colA, 5); table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); assertColumn(table, row1, colA, 7); // test multiple increment columns byte[] row2 = Bytes.toBytes("row2"); byte[] colB = Bytes.toBytes("b"); // increment A and B twice at the same timestamp table.put(newIncrement(row2, colA, 1, 1)); table.put(newIncrement(row2, colB, 1, 1)); table.put(newIncrement(row2, colA, 2, 1)); table.put(newIncrement(row2, colB, 2, 1)); // increment A once more table.put(newIncrement(row2, colA, 1)); assertColumns(table, row2, new byte[][]{colA, colB}, new long[]{3, 2}); // overwrite B with a new put table.put(tableUtil.buildPut(row2).add(FAMILY, colB, ts++, Bytes.toBytes(10L)).build()); assertColumns(table, row2, new byte[][]{colA, colB}, new long[]{3, 10}); } } @Test public void testIncrementsCompaction() throws Exception { // In this test we verify that squashing delta-increments during flush or compaction works as designed. TableId tableId = TableId.from(Id.Namespace.DEFAULT, "incrementCompactTest"); HTable table = createTable(tableId); byte[] tableBytes = table.getTableName(); try { byte[] colA = Bytes.toBytes("a"); byte[] row1 = Bytes.toBytes("row1"); // do some increments table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); assertColumn(table, row1, colA, 3); TEST_HBASE.forceRegionFlush(tableBytes); // verify increments after flush assertColumn(table, row1, colA, 3); // do some more increments table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); // verify increments merged well from hstore and memstore assertColumn(table, row1, colA, 6); TEST_HBASE.forceRegionFlush(tableBytes); // verify increments merged well into hstores assertColumn(table, row1, colA, 6); // do another iteration to verify that multiple "squashed" increments merged well at scan and at flush table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); assertColumn(table, row1, colA, 9); TEST_HBASE.forceRegionFlush(tableBytes); assertColumn(table, row1, colA, 9); // verify increments merged well on minor compaction TEST_HBASE.forceRegionCompact(tableBytes, false); assertColumn(table, row1, colA, 9); // another round of increments to verify that merged on compaction merges well with memstore and with new hstores table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); assertColumn(table, row1, colA, 12); TEST_HBASE.forceRegionFlush(tableBytes); assertColumn(table, row1, colA, 12); // do same, but with major compaction // verify increments merged well on minor compaction TEST_HBASE.forceRegionCompact(tableBytes, true); assertColumn(table, row1, colA, 12); // another round of increments to verify that merged on compaction merges well with memstore and with new hstores table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); table.put(newIncrement(row1, colA, 1)); assertColumn(table, row1, colA, 15); TEST_HBASE.forceRegionFlush(tableBytes); assertColumn(table, row1, colA, 15); } finally { table.close(); } } @Test public void testIncrementsCompactionUnlimBound() throws Exception { try (RegionWrapper region = createRegion(TableId.from(Id.Namespace.DEFAULT, "testIncrementsCompactionsUnlimBound"), ImmutableMap.<String, String>builder() .put(IncrementHandlerState.PROPERTY_TRANSACTIONAL, "false").build())) { region.initialize(); byte[] colA = Bytes.toBytes("a"); byte[] row1 = Bytes.toBytes("row1"); // do some increments region.put(newIncrement(row1, colA, 1)); region.put(newIncrement(row1, colA, 1)); region.put(newIncrement(row1, colA, 1)); region.flush(); // verify increments merged after flush assertSingleVersionColumn(region, row1, colA, 3); // do some more increments region.put(newIncrement(row1, colA, 1)); region.put(newIncrement(row1, colA, 1)); region.put(newIncrement(row1, colA, 1)); region.flush(); region.compact(true); // verify increments merged well into hstores assertSingleVersionColumn(region, row1, colA, 6); } } @Test public void testNonTransactionalMixed() throws Exception { // test mix of increment, put and delete operations TableId tableId = TableId.from(Id.Namespace.DEFAULT, "testNonTransactionalMixed"); byte[] row1 = Bytes.toBytes("r1"); byte[] col = Bytes.toBytes("c"); try (HTable table = createTable(tableId)) { // perform 100 increments on a column for (int i = 0; i < 100; i++) { table.put(newIncrement(row1, col, 1)); } assertColumn(table, row1, col, 100); // do a new put on the column table.put(tableUtil.buildPut(row1).add(FAMILY, col, Bytes.toBytes(11L)).build()); assertColumn(table, row1, col, 11); // perform a delete on the column Delete delete = tableUtil.buildDelete(row1) .deleteColumns(FAMILY, col) .build(); // use batch to work around a bug in delete coprocessor hooks on HBase 0.94 table.batch(Lists.newArrayList(delete)); Get get = tableUtil.buildGet(row1).build(); Result result = table.get(get); LOG.info("Get after delete returned " + result); assertTrue(result.isEmpty()); // perform 100 increments on a column for (int i = 0; i < 100; i++) { table.put(newIncrement(row1, col, 1)); } assertColumn(table, row1, col, 100); // perform a family delete delete = tableUtil.buildDelete(row1) .deleteFamily(FAMILY) .build(); // use batch to work around a bug in delete coprocessor hooks on HBase 0.94 table.batch(Lists.newArrayList(delete)); get = tableUtil.buildGet(row1).build(); result = table.get(get); LOG.info("Get after delete returned " + result); assertTrue(result.isEmpty()); // do 100 more increments for (int i = 0; i < 100; i++) { table.put(newIncrement(row1, col, 1)); } assertColumn(table, row1, col, 100); // perform a row delete delete = tableUtil.buildDelete(row1).build(); // use batch to work around a bug in delete coprocessor hooks on HBase 0.94 table.batch(Lists.newArrayList(delete)); get = tableUtil.buildGet(row1).build(); result = table.get(get); LOG.info("Get after delete returned " + result); assertTrue(result.isEmpty()); // do 100 more increments for (int i = 0; i < 100; i++) { table.put(newIncrement(row1, col, 1)); } assertColumn(table, row1, col, 100); } } /** * Verifies that time-to-live based expiration of data is applied correctly when the {@code IncrementHandler} * coprocessor is generating timestamps, ie. for non-transactional writes. TTL-based expiration of increment * values follows a couple of rule: * <ol> * <li>delta writes (increments) are never TTL'd</li> * <li>a normal put is only TTL'd if not preceeded by newer increments in the same column</li> * </ol> */ @Test public void testNonTransactionalTTL() throws Exception { byte[] row = Bytes.toBytes("r1"); byte[] col = Bytes.toBytes("c"); try (RegionWrapper region = createRegion(TableId.from(Id.Namespace.DEFAULT, "testNonTransactionalTTL"), ImmutableMap.<String, String>builder() .put(IncrementHandlerState.PROPERTY_TRANSACTIONAL, "false") .put(TxConstants.PROPERTY_TTL, "50") .build())) { region.initialize(); SettableTimestampOracle timeOracle = new SettableTimestampOracle(); region.setCoprocessorTimestampOracle(timeOracle); // test that we do not apply TTL in the middle of a set of increments long now = System.currentTimeMillis(); for (int i = 100; i > 0; i--) { timeOracle.setCurrentTime((now - i) * IncrementHandlerState.MAX_TS_PER_MS); // timestamp will be overridden by IncrementHandler coprocessor region.put(newIncrement(row, col, Integer.MAX_VALUE, 1)); } // reset "current time" timeOracle.setCurrentTime(now * IncrementHandlerState.MAX_TS_PER_MS); List<ColumnCell> results = Lists.newArrayList(); assertFalse(region.scanRegion(results, row)); // verify we have 100 individual cells, one per increment assertEquals(100, results.size()); for (int i = 0; i < results.size(); i++) { ColumnCell cell = results.get(i); assertEquals(1L, Bytes.toLong(cell.getValue(), 2)); Assert.assertEquals((now - i - 1) * IncrementHandlerState.MAX_TS_PER_MS, cell.getTimestamp()); } // verify that when summing during flush we return the correct sum // run a flush, verify that all cells are included in the summed value region.flush(); results.clear(); assertFalse(region.scanRegion(results, row)); // verify 1 increment cell now exists assertEquals(1, results.size()); assertEquals(100L, Bytes.toLong(results.get(0).getValue(), 2)); // should have the timestamp from the most recent increment Assert.assertEquals((now - 1) * IncrementHandlerState.MAX_TS_PER_MS, results.get(0).getTimestamp()); // test that we do not apply TTL to a put terminating a set of increments byte[] row2 = Bytes.toBytes("r2"); // first add a full put region.put(tableUtil.buildPut(row2).add(FAMILY, col, Bytes.toBytes(50L)).build()); // move 51 msec into the future, so that the previous put is behind the TTL now = now + 51; // add some increments for (int i = 10; i > 0; i--) { timeOracle.setCurrentTime((now - i) * IncrementHandlerState.MAX_TS_PER_MS); // timestamp will be overridden by IncrementHandler coprocessor region.put(newIncrement(row2, col, Integer.MAX_VALUE, 1)); } // reset "current time" timeOracle.setCurrentTime(now * IncrementHandlerState.MAX_TS_PER_MS); results.clear(); assertFalse(region.scanRegion(results, row2)); assertEquals(11, results.size()); // first 10 cells should be the increments for (int i = 0; i < 10; i++) { ColumnCell cell = results.get(i); Assert.assertEquals((now - i - 1) * IncrementHandlerState.MAX_TS_PER_MS, cell.getTimestamp()); assertEquals(1L, Bytes.toLong(cell.getValue(), 2)); } // last should be the full put ColumnCell cell = results.get(10); Assert.assertEquals((now - 51) * IncrementHandlerState.MAX_TS_PER_MS, cell.getTimestamp()); assertEquals(50L, Bytes.toLong(cell.getValue())); // do a compaction region.flush(); region.compact(true); // verify that the 10 increments and 1 put are summed to 1 cell (as a normal put) results.clear(); assertFalse(region.scanRegion(results, row2)); assertEquals(1, results.size()); assertEquals(60L, Bytes.toLong(results.get(0).getValue())); Assert.assertEquals((now - 1) * IncrementHandlerState.MAX_TS_PER_MS, results.get(0).getTimestamp()); // test that we apply TTL to a put preceeded by a non-TTL'd put // go 50 msec into the future now = now + 50; timeOracle.setCurrentTime(now * IncrementHandlerState.MAX_TS_PER_MS); // do another full put region.put(tableUtil.buildPut(row2).add(FAMILY, col, Bytes.toBytes(99L)).build()); // run a compaction to apply TTL region.compact(true); // the previously summed value should now be gone results.clear(); assertFalse(region.scanRegion(results, row2)); assertEquals(1, results.size()); assertEquals(99L, Bytes.toLong(results.get(0).getValue())); Assert.assertEquals(now * IncrementHandlerState.MAX_TS_PER_MS, results.get(0).getTimestamp()); // test that we apply TTL to a standalone put byte[] row3 = Bytes.toBytes("r3"); region.put(tableUtil.buildPut(row3).add(FAMILY, col, Bytes.toBytes(11L)).build()); results.clear(); assertFalse(region.scanRegion(results, row3)); assertEquals(1, results.size()); assertEquals(11L, Bytes.toLong(results.get(0).getValue())); // advance 51 msec into the future now = now + 51; timeOracle.setCurrentTime(now * IncrementHandlerState.MAX_TS_PER_MS); region.flush(); region.compact(true); // the stand alone put should be gone results.clear(); assertFalse(region.scanRegion(results, row3)); assertEquals(0, results.size()); } } public Put newIncrement(byte[] row, byte[] column, long value) { return newIncrement(row, column, ts++, value); } public Put newIncrement(byte[] row, byte[] column, long timestamp, long value) { return tableUtil.buildPut(row) .add(FAMILY, column, timestamp, Bytes.toBytes(value)) .setAttribute(HBaseTable.DELTA_WRITE, EMPTY_BYTES) .build(); } public abstract void assertColumn(HTable table, byte[] row, byte[] col, long expected) throws Exception; public void assertSingleVersionColumn(RegionWrapper region, byte[] row, byte[] col, long expected) throws Exception { List<ColumnCell> results = Lists.newArrayList(); Assert.assertFalse(region.scanRegion(results, row, new byte[][]{col})); Assert.assertEquals(1, results.size()); byte[] value = results.get(0).getValue(); // note: it may be stored as increment delta even after merge on flush/compact long longValue = Bytes.toLong(value, value.length > Bytes.SIZEOF_LONG ? IncrementHandlerState.DELTA_MAGIC_PREFIX.length : 0); Assert.assertEquals(expected, longValue); } public abstract void assertColumns(HTable table, byte[] row, byte[][] cols, long[] expected) throws Exception; public abstract RegionWrapper createRegion(TableId tableId, Map<String, String> familyProperties) throws Exception; public abstract HTable createTable(TableId tableId) throws Exception; public static class ColumnCell { private final byte[] row; private final byte[] family; private final byte[] qualifier; private final long timestamp; private final byte[] value; public ColumnCell(byte[] row, byte[] family, byte[] qualifier, long timestamp, byte[] value) { this.row = row; this.family = family; this.qualifier = qualifier; this.timestamp = timestamp; this.value = value; } public byte[] getRow() { return row; } public byte[] getFamily() { return family; } public byte[] getQualifier() { return qualifier; } public long getTimestamp() { return timestamp; } public byte[] getValue() { return value; } } public interface RegionWrapper extends Closeable { void initialize() throws IOException; void put(Put put) throws IOException; boolean scanRegion(List<ColumnCell> results, byte[] startRow) throws IOException; boolean scanRegion(List<ColumnCell> results, byte[] startRow, byte[][] column) throws IOException; boolean flush() throws IOException; void compact(boolean majorCompact) throws IOException; void setCoprocessorTimestampOracle(TimestampOracle timeOracle); } }