| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.hadoop.hbase; |
| |
| import static org.junit.Assert.assertArrayEquals; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| import org.apache.hadoop.hbase.client.Delete; |
| import org.apache.hadoop.hbase.client.Put; |
| import org.apache.hadoop.hbase.client.RegionInfo; |
| import org.apache.hadoop.hbase.client.Result; |
| import org.apache.hadoop.hbase.client.ResultScanner; |
| import org.apache.hadoop.hbase.client.Scan; |
| import org.apache.hadoop.hbase.client.Table; |
| import org.apache.hadoop.hbase.filter.ColumnPrefixFilter; |
| import org.apache.hadoop.hbase.filter.ColumnRangeFilter; |
| import org.apache.hadoop.hbase.filter.Filter; |
| import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter; |
| import org.apache.hadoop.hbase.filter.FirstKeyValueMatchingQualifiersFilter; |
| import org.apache.hadoop.hbase.filter.RandomRowFilter; |
| import org.apache.hadoop.hbase.testclassification.LargeTests; |
| import org.apache.hadoop.hbase.util.Bytes; |
| import org.apache.hadoop.hbase.util.ClassSize; |
| import org.apache.hadoop.hbase.util.Pair; |
| import org.junit.AfterClass; |
| import org.junit.BeforeClass; |
| import org.junit.ClassRule; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.experimental.categories.Category; |
| import org.junit.rules.TestName; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * These tests are focused on testing how partial results appear to a client. Partial results are |
| * {@link Result}s that contain only a portion of a row's complete list of cells. Partial results |
| * are formed when the server breaches its maximum result size when trying to service a client's RPC |
| * request. It is the responsibility of the scanner on the client side to recognize when partial |
| * results have been returned and to take action to form the complete results. |
| * <p> |
| * Unless the flag {@link Scan#setAllowPartialResults(boolean)} has been set to true, the caller of |
| * {@link ResultScanner#next()} should never see partial results. |
| */ |
| @Category(LargeTests.class) |
| public class TestPartialResultsFromClientSide { |
| |
| @ClassRule |
| public static final HBaseClassTestRule CLASS_RULE = |
| HBaseClassTestRule.forClass(TestPartialResultsFromClientSide.class); |
| |
| private static final Logger LOG = LoggerFactory.getLogger(TestPartialResultsFromClientSide.class); |
| |
| private final static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); |
| private final static int MINICLUSTER_SIZE = 5; |
| private static Table TABLE = null; |
| |
| /** |
| * Table configuration |
| */ |
| private static TableName TABLE_NAME = TableName.valueOf("testTable"); |
| |
| private static int NUM_ROWS = 5; |
| private static byte[] ROW = Bytes.toBytes("testRow"); |
| private static byte[][] ROWS = HTestConst.makeNAscii(ROW, NUM_ROWS); |
| |
| // Should keep this value below 10 to keep generation of expected kv's simple. If above 10 then |
| // table/row/cf1/... will be followed by table/row/cf10/... instead of table/row/cf2/... which |
| // breaks the simple generation of expected kv's |
| private static int NUM_FAMILIES = 10; |
| private static byte[] FAMILY = Bytes.toBytes("testFamily"); |
| private static byte[][] FAMILIES = HTestConst.makeNAscii(FAMILY, NUM_FAMILIES); |
| |
| private static int NUM_QUALIFIERS = 10; |
| private static byte[] QUALIFIER = Bytes.toBytes("testQualifier"); |
| private static byte[][] QUALIFIERS = HTestConst.makeNAscii(QUALIFIER, NUM_QUALIFIERS); |
| |
| private static int VALUE_SIZE = 1024; |
| private static byte[] VALUE = Bytes.createMaxByteArray(VALUE_SIZE); |
| |
| private static int NUM_COLS = NUM_FAMILIES * NUM_QUALIFIERS; |
| |
| // Approximation of how large the heap size of cells in our table. Should be accessed through |
| // getCellHeapSize(). |
| private static long CELL_HEAP_SIZE = -1; |
| |
| private static long timeout = 10000; |
| |
| @Rule |
| public TestName name = new TestName(); |
| |
| @BeforeClass |
| public static void setUpBeforeClass() throws Exception { |
| TEST_UTIL.getConfiguration().setLong(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, timeout); |
| TEST_UTIL.startMiniCluster(MINICLUSTER_SIZE); |
| TEST_UTIL.getAdmin().balancerSwitch(false, true); |
| TABLE = createTestTable(TABLE_NAME, ROWS, FAMILIES, QUALIFIERS, VALUE); |
| } |
| |
| static Table createTestTable(TableName name, byte[][] rows, byte[][] families, |
| byte[][] qualifiers, byte[] cellValue) throws IOException { |
| Table ht = TEST_UTIL.createTable(name, families); |
| List<Put> puts = createPuts(rows, families, qualifiers, cellValue); |
| ht.put(puts); |
| |
| return ht; |
| } |
| |
| @AfterClass |
| public static void tearDownAfterClass() throws Exception { |
| TEST_UTIL.shutdownMiniCluster(); |
| } |
| |
| /** |
| * Ensure that the expected key values appear in a result returned from a scanner that is |
| * combining partial results into complete results |
| * @throws Exception |
| */ |
| @Test |
| public void testExpectedValuesOfPartialResults() throws Exception { |
| testExpectedValuesOfPartialResults(false); |
| testExpectedValuesOfPartialResults(true); |
| } |
| |
| public void testExpectedValuesOfPartialResults(boolean reversed) throws Exception { |
| Scan partialScan = new Scan(); |
| partialScan.readAllVersions(); |
| // Max result size of 1 ensures that each RPC request will return a single cell. The scanner |
| // will need to reconstruct the results into a complete result before returning to the caller |
| partialScan.setMaxResultSize(1); |
| partialScan.setReversed(reversed); |
| ResultScanner partialScanner = TABLE.getScanner(partialScan); |
| |
| final int startRow = reversed ? ROWS.length - 1 : 0; |
| final int endRow = reversed ? -1 : ROWS.length; |
| final int loopDelta = reversed ? -1 : 1; |
| String message; |
| |
| for (int row = startRow; row != endRow; row = row + loopDelta) { |
| message = "Ensuring the expected keyValues are present for row " + row; |
| List<Cell> expectedKeyValues = createKeyValuesForRow(ROWS[row], FAMILIES, QUALIFIERS, VALUE); |
| Result result = partialScanner.next(); |
| assertFalse(result.mayHaveMoreCellsInRow()); |
| verifyResult(result, expectedKeyValues, message); |
| } |
| |
| partialScanner.close(); |
| } |
| |
| /** |
| * Ensure that we only see Results marked as partial when the allowPartial flag is set |
| * @throws Exception |
| */ |
| @Test |
| public void testAllowPartialResults() throws Exception { |
| Scan scan = new Scan(); |
| scan.setAllowPartialResults(true); |
| scan.setMaxResultSize(1); |
| ResultScanner scanner = TABLE.getScanner(scan); |
| Result result = scanner.next(); |
| |
| assertTrue(result != null); |
| assertTrue(result.mayHaveMoreCellsInRow()); |
| assertTrue(result.rawCells() != null); |
| assertTrue(result.rawCells().length == 1); |
| |
| scanner.close(); |
| |
| scan.setAllowPartialResults(false); |
| scanner = TABLE.getScanner(scan); |
| result = scanner.next(); |
| |
| assertTrue(result != null); |
| assertTrue(!result.mayHaveMoreCellsInRow()); |
| assertTrue(result.rawCells() != null); |
| assertTrue(result.rawCells().length == NUM_COLS); |
| |
| scanner.close(); |
| } |
| |
| /** |
| * Ensure that the results returned from a scanner that retrieves all results in a single RPC call |
| * matches the results that are returned from a scanner that must incrementally combine partial |
| * results into complete results. A variety of scan configurations can be tested |
| * @throws Exception |
| */ |
| @Test |
| public void testEquivalenceOfScanResults() throws Exception { |
| Scan oneShotScan = new Scan(); |
| oneShotScan.setMaxResultSize(Long.MAX_VALUE); |
| |
| Scan partialScan = new Scan(oneShotScan); |
| partialScan.setMaxResultSize(1); |
| |
| testEquivalenceOfScanResults(TABLE, oneShotScan, partialScan); |
| } |
| |
| public void testEquivalenceOfScanResults(Table table, Scan scan1, Scan scan2) throws Exception { |
| ResultScanner scanner1 = table.getScanner(scan1); |
| ResultScanner scanner2 = table.getScanner(scan2); |
| |
| Result r1 = null; |
| Result r2 = null; |
| int count = 0; |
| |
| while ((r1 = scanner1.next()) != null) { |
| r2 = scanner2.next(); |
| |
| assertTrue(r2 != null); |
| compareResults(r1, r2, "Comparing result #" + count); |
| count++; |
| } |
| |
| r2 = scanner2.next(); |
| assertTrue("r2: " + r2 + " Should be null", r2 == null); |
| |
| scanner1.close(); |
| scanner2.close(); |
| } |
| |
| /** |
| * Order of cells in partial results matches the ordering of cells from complete results |
| * @throws Exception |
| */ |
| @Test |
| public void testOrderingOfCellsInPartialResults() throws Exception { |
| Scan scan = new Scan(); |
| |
| for (int col = 1; col <= NUM_COLS; col++) { |
| scan.setMaxResultSize(getResultSizeForNumberOfCells(col)); |
| testOrderingOfCellsInPartialResults(scan); |
| |
| // Test again with a reversed scanner |
| scan.setReversed(true); |
| testOrderingOfCellsInPartialResults(scan); |
| } |
| } |
| |
| public void testOrderingOfCellsInPartialResults(final Scan basePartialScan) throws Exception { |
| // Scan that retrieves results in pieces (partials). By setting allowPartialResults to be true |
| // the results will NOT be reconstructed and instead the caller will see the partial results |
| // returned by the server |
| Scan partialScan = new Scan(basePartialScan); |
| partialScan.setAllowPartialResults(true); |
| ResultScanner partialScanner = TABLE.getScanner(partialScan); |
| |
| // Scan that retrieves all table results in single RPC request |
| Scan oneShotScan = new Scan(basePartialScan); |
| oneShotScan.setMaxResultSize(Long.MAX_VALUE); |
| oneShotScan.setCaching(ROWS.length); |
| ResultScanner oneShotScanner = TABLE.getScanner(oneShotScan); |
| |
| Result oneShotResult = oneShotScanner.next(); |
| Result partialResult = null; |
| int iterationCount = 0; |
| |
| while (oneShotResult != null && oneShotResult.rawCells() != null) { |
| List<Cell> aggregatePartialCells = new ArrayList<>(); |
| do { |
| partialResult = partialScanner.next(); |
| assertTrue("Partial Result is null. iteration: " + iterationCount, partialResult != null); |
| assertTrue("Partial cells are null. iteration: " + iterationCount, |
| partialResult.rawCells() != null); |
| |
| for (Cell c : partialResult.rawCells()) { |
| aggregatePartialCells.add(c); |
| } |
| } while (partialResult.mayHaveMoreCellsInRow()); |
| |
| assertTrue("Number of cells differs. iteration: " + iterationCount, |
| oneShotResult.rawCells().length == aggregatePartialCells.size()); |
| final Cell[] oneShotCells = oneShotResult.rawCells(); |
| for (int cell = 0; cell < oneShotCells.length; cell++) { |
| Cell oneShotCell = oneShotCells[cell]; |
| Cell partialCell = aggregatePartialCells.get(cell); |
| |
| assertTrue("One shot cell was null", oneShotCell != null); |
| assertTrue("Partial cell was null", partialCell != null); |
| assertTrue("Cell differs. oneShotCell:" + oneShotCell + " partialCell:" + partialCell, |
| oneShotCell.equals(partialCell)); |
| } |
| |
| oneShotResult = oneShotScanner.next(); |
| iterationCount++; |
| } |
| |
| assertTrue(partialScanner.next() == null); |
| |
| partialScanner.close(); |
| oneShotScanner.close(); |
| } |
| |
| /** |
| * Setting the max result size allows us to control how many cells we expect to see on each call |
| * to next on the scanner. Test a variety of different sizes for correctness |
| * @throws Exception |
| */ |
| @Test |
| public void testExpectedNumberOfCellsPerPartialResult() throws Exception { |
| Scan scan = new Scan(); |
| testExpectedNumberOfCellsPerPartialResult(scan); |
| |
| scan.setReversed(true); |
| testExpectedNumberOfCellsPerPartialResult(scan); |
| } |
| |
| public void testExpectedNumberOfCellsPerPartialResult(Scan baseScan) throws Exception { |
| for (int expectedCells = 1; expectedCells <= NUM_COLS; expectedCells++) { |
| testExpectedNumberOfCellsPerPartialResult(baseScan, expectedCells); |
| } |
| } |
| |
| public void testExpectedNumberOfCellsPerPartialResult(Scan baseScan, int expectedNumberOfCells) |
| throws Exception { |
| |
| if (LOG.isInfoEnabled()) LOG.info("groupSize:" + expectedNumberOfCells); |
| |
| // Use the cellHeapSize to set maxResultSize such that we know how many cells to expect back |
| // from the call. The returned results should NOT exceed expectedNumberOfCells but may be less |
| // than it in cases where expectedNumberOfCells is not an exact multiple of the number of |
| // columns in the table. |
| Scan scan = new Scan(baseScan); |
| scan.setAllowPartialResults(true); |
| scan.setMaxResultSize(getResultSizeForNumberOfCells(expectedNumberOfCells)); |
| |
| ResultScanner scanner = TABLE.getScanner(scan); |
| Result result = null; |
| byte[] prevRow = null; |
| while ((result = scanner.next()) != null) { |
| assertTrue(result.rawCells() != null); |
| |
| // Cases when cell count won't equal expectedNumberOfCells: |
| // 1. Returned result is the final result needed to form the complete result for that row |
| // 2. It is the first result we have seen for that row and thus may have been fetched as |
| // the last group of cells that fit inside the maxResultSize |
| assertTrue( |
| "Result's cell count differed from expected number. result: " + result, |
| result.rawCells().length == expectedNumberOfCells || !result.mayHaveMoreCellsInRow() |
| || !Bytes.equals(prevRow, result.getRow())); |
| prevRow = result.getRow(); |
| } |
| |
| scanner.close(); |
| } |
| |
| /** |
| * @return The approximate heap size of a cell in the test table. All cells should have |
| * approximately the same heap size, so the value is cached to avoid repeating the |
| * calculation |
| * @throws Exception |
| */ |
| private long getCellHeapSize() throws Exception { |
| if (CELL_HEAP_SIZE == -1) { |
| // Do a partial scan that will return a single result with a single cell |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(2); |
| scan.setAllowPartialResults(true); |
| ResultScanner scanner = TABLE.getScanner(scan); |
| |
| Result result = scanner.next(); |
| |
| assertTrue(result != null); |
| assertTrue(result.rawCells() != null); |
| assertTrue(result.rawCells().length == 1); |
| |
| // Estimate the cell heap size. One difference is that on server side, the KV Heap size is |
| // estimated differently in case the cell is backed up by MSLAB byte[] (no overhead for |
| // backing array). Thus below calculation is a bit brittle. |
| CELL_HEAP_SIZE = result.rawCells()[0].heapSize() - (ClassSize.ARRAY + 3); |
| if (LOG.isInfoEnabled()) LOG.info("Cell heap size: " + CELL_HEAP_SIZE); |
| scanner.close(); |
| } |
| |
| return CELL_HEAP_SIZE; |
| } |
| |
| /** |
| * @param numberOfCells |
| * @return the result size that should be used in {@link Scan#setMaxResultSize(long)} if you want |
| * the server to return exactly numberOfCells cells |
| * @throws Exception |
| */ |
| private long getResultSizeForNumberOfCells(int numberOfCells) throws Exception { |
| return getCellHeapSize() * numberOfCells; |
| } |
| |
| /** |
| * Test various combinations of batching and partial results for correctness |
| */ |
| @Test |
| public void testPartialResultsAndBatch() throws Exception { |
| for (int batch = 1; batch <= NUM_COLS / 4; batch++) { |
| for (int cellsPerPartial = 1; cellsPerPartial <= NUM_COLS / 4; cellsPerPartial++) { |
| testPartialResultsAndBatch(batch, cellsPerPartial); |
| } |
| } |
| } |
| |
| public void testPartialResultsAndBatch(final int batch, final int cellsPerPartialResult) |
| throws Exception { |
| if (LOG.isInfoEnabled()) { |
| LOG.info("batch: " + batch + " cellsPerPartialResult: " + cellsPerPartialResult); |
| } |
| |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(getResultSizeForNumberOfCells(cellsPerPartialResult)); |
| scan.setBatch(batch); |
| ResultScanner scanner = TABLE.getScanner(scan); |
| Result result = scanner.next(); |
| int repCount = 0; |
| |
| while ((result = scanner.next()) != null) { |
| assertTrue(result.rawCells() != null); |
| |
| if (result.mayHaveMoreCellsInRow()) { |
| final String error = |
| "Cells:" + result.rawCells().length + " Batch size:" + batch |
| + " cellsPerPartialResult:" + cellsPerPartialResult + " rep:" + repCount; |
| assertTrue(error, result.rawCells().length == batch); |
| } else { |
| assertTrue(result.rawCells().length <= batch); |
| } |
| repCount++; |
| } |
| |
| scanner.close(); |
| } |
| |
| /** |
| * Test the method {@link Result#createCompleteResult(Iterable)} |
| * @throws Exception |
| */ |
| @Test |
| public void testPartialResultsReassembly() throws Exception { |
| Scan scan = new Scan(); |
| testPartialResultsReassembly(scan); |
| scan.setReversed(true); |
| testPartialResultsReassembly(scan); |
| } |
| |
| public void testPartialResultsReassembly(Scan scanBase) throws Exception { |
| Scan partialScan = new Scan(scanBase); |
| partialScan.setMaxResultSize(1); |
| partialScan.setAllowPartialResults(true); |
| ResultScanner partialScanner = TABLE.getScanner(partialScan); |
| |
| Scan oneShotScan = new Scan(scanBase); |
| oneShotScan.setMaxResultSize(Long.MAX_VALUE); |
| ResultScanner oneShotScanner = TABLE.getScanner(oneShotScan); |
| |
| ArrayList<Result> partials = new ArrayList<>(); |
| for (int i = 0; i < NUM_ROWS; i++) { |
| Result partialResult = null; |
| Result completeResult = null; |
| Result oneShotResult = null; |
| partials.clear(); |
| |
| do { |
| partialResult = partialScanner.next(); |
| partials.add(partialResult); |
| } while (partialResult != null && partialResult.mayHaveMoreCellsInRow()); |
| |
| completeResult = Result.createCompleteResult(partials); |
| oneShotResult = oneShotScanner.next(); |
| |
| compareResults(completeResult, oneShotResult, null); |
| } |
| |
| assertTrue(oneShotScanner.next() == null); |
| assertTrue(partialScanner.next() == null); |
| |
| oneShotScanner.close(); |
| partialScanner.close(); |
| } |
| |
| /** |
| * When reconstructing the complete result from its partials we ensure that the row of each |
| * partial result is the same. If one of the rows differs, an exception is thrown. |
| */ |
| @Test |
| public void testExceptionThrownOnMismatchedPartialResults() throws IOException { |
| assertTrue(NUM_ROWS >= 2); |
| |
| ArrayList<Result> partials = new ArrayList<>(); |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(Long.MAX_VALUE); |
| ResultScanner scanner = TABLE.getScanner(scan); |
| Result r1 = scanner.next(); |
| partials.add(r1); |
| Result r2 = scanner.next(); |
| partials.add(r2); |
| |
| assertFalse(Bytes.equals(r1.getRow(), r2.getRow())); |
| |
| try { |
| Result.createCompleteResult(partials); |
| fail("r1 and r2 are from different rows. It should not be possible to combine them into" |
| + " a single result"); |
| } catch (IOException e) { |
| } |
| |
| scanner.close(); |
| } |
| |
| /** |
| * When a scan has a filter where {@link org.apache.hadoop.hbase.filter.Filter#hasFilterRow()} is |
| * true, the scanner should not return partial results. The scanner cannot return partial results |
| * because the entire row needs to be read for the include/exclude decision to be made |
| */ |
| @Test |
| public void testNoPartialResultsWhenRowFilterPresent() throws Exception { |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(1); |
| scan.setAllowPartialResults(true); |
| // If a filter hasFilter() is true then partial results should not be returned else filter |
| // application server side would break. |
| scan.setFilter(new RandomRowFilter(1.0f)); |
| ResultScanner scanner = TABLE.getScanner(scan); |
| |
| Result r = null; |
| while ((r = scanner.next()) != null) { |
| assertFalse(r.mayHaveMoreCellsInRow()); |
| } |
| |
| scanner.close(); |
| } |
| |
| /** |
| * Examine the interaction between the maxResultSize and caching. If the caching limit is reached |
| * before the maxResultSize limit, we should not see partial results. On the other hand, if the |
| * maxResultSize limit is reached before the caching limit, it is likely that partial results will |
| * be seen. |
| * @throws Exception |
| */ |
| @Test |
| public void testPartialResultsAndCaching() throws Exception { |
| for (int caching = 1; caching <= NUM_ROWS; caching++) { |
| for (int maxResultRows = 0; maxResultRows <= NUM_ROWS; maxResultRows++) { |
| testPartialResultsAndCaching(maxResultRows, caching); |
| } |
| } |
| } |
| |
| /** |
| * @param resultSizeRowLimit The row limit that will be enforced through maxResultSize |
| * @param cachingRowLimit The row limit that will be enforced through caching |
| */ |
| public void testPartialResultsAndCaching(int resultSizeRowLimit, int cachingRowLimit) |
| throws Exception { |
| Scan scan = new Scan(); |
| scan.setAllowPartialResults(true); |
| |
| // The number of cells specified in the call to getResultSizeForNumberOfCells is offset to |
| // ensure that the result size we specify is not an exact multiple of the number of cells |
| // in a row. This ensures that partial results will be returned when the result size limit |
| // is reached before the caching limit. |
| int cellOffset = NUM_COLS / 3; |
| long maxResultSize = getResultSizeForNumberOfCells(resultSizeRowLimit * NUM_COLS + cellOffset); |
| scan.setMaxResultSize(maxResultSize); |
| scan.setCaching(cachingRowLimit); |
| |
| try (ResultScanner scanner = TABLE.getScanner(scan)) { |
| Result r = null; |
| // Approximate the number of rows we expect will fit into the specified max rsult size. If |
| // this approximation is less than caching, then we expect that the max result size limit will |
| // be hit before the caching limit and thus partial results may be seen |
| boolean expectToSeePartialResults = resultSizeRowLimit < cachingRowLimit; |
| while ((r = scanner.next()) != null) { |
| assertTrue(!r.mayHaveMoreCellsInRow() || expectToSeePartialResults); |
| } |
| } |
| } |
| |
| /** |
| * Make puts to put the input value into each combination of row, family, and qualifier |
| * @param rows the rows to use |
| * @param families the families to use |
| * @param qualifiers the qualifiers to use |
| * @param value the values to use |
| * @return the dot product of the given rows, families, qualifiers, and values |
| * @throws IOException if there is a problem creating one of the Put objects |
| */ |
| static ArrayList<Put> createPuts(byte[][] rows, byte[][] families, byte[][] qualifiers, |
| byte[] value) throws IOException { |
| Put put; |
| ArrayList<Put> puts = new ArrayList<>(); |
| |
| for (int row = 0; row < rows.length; row++) { |
| put = new Put(rows[row]); |
| for (int fam = 0; fam < families.length; fam++) { |
| for (int qual = 0; qual < qualifiers.length; qual++) { |
| KeyValue kv = new KeyValue(rows[row], families[fam], qualifiers[qual], qual, value); |
| put.add(kv); |
| } |
| } |
| puts.add(put); |
| } |
| |
| return puts; |
| } |
| |
| /** |
| * Make key values to represent each possible combination of family and qualifier in the specified |
| * row. |
| * @param row the row to use |
| * @param families the families to use |
| * @param qualifiers the qualifiers to use |
| * @param value the values to use |
| * @return the dot product of the given families, qualifiers, and values for a given row |
| */ |
| static ArrayList<Cell> createKeyValuesForRow(byte[] row, byte[][] families, byte[][] qualifiers, |
| byte[] value) { |
| ArrayList<Cell> outList = new ArrayList<>(); |
| for (int fam = 0; fam < families.length; fam++) { |
| for (int qual = 0; qual < qualifiers.length; qual++) { |
| outList.add(new KeyValue(row, families[fam], qualifiers[qual], qual, value)); |
| } |
| } |
| return outList; |
| } |
| |
| /** |
| * Verifies that result contains all the key values within expKvList. Fails the test otherwise |
| * @param result |
| * @param expKvList |
| * @param msg |
| */ |
| static void verifyResult(Result result, List<Cell> expKvList, String msg) { |
| if (LOG.isInfoEnabled()) { |
| LOG.info(msg); |
| LOG.info("Expected count: " + expKvList.size()); |
| LOG.info("Actual count: " + result.size()); |
| } |
| |
| if (expKvList.isEmpty()) return; |
| |
| int i = 0; |
| for (Cell kv : result.rawCells()) { |
| if (i >= expKvList.size()) { |
| break; // we will check the size later |
| } |
| |
| Cell kvExp = expKvList.get(i++); |
| assertTrue("Not equal. get kv: " + kv.toString() + " exp kv: " + kvExp.toString(), |
| kvExp.equals(kv)); |
| } |
| |
| assertEquals(expKvList.size(), result.size()); |
| } |
| |
| /** |
| * Compares two results and fails the test if the results are different |
| * @param r1 |
| * @param r2 |
| * @param message |
| */ |
| static void compareResults(Result r1, Result r2, final String message) { |
| if (LOG.isInfoEnabled()) { |
| if (message != null) LOG.info(message); |
| LOG.info("r1: " + r1); |
| LOG.info("r2: " + r2); |
| } |
| |
| final String failureMessage = "Results r1:" + r1 + " \nr2:" + r2 + " are not equivalent"; |
| if (r1 == null && r2 == null) fail(failureMessage); |
| else if (r1 == null || r2 == null) fail(failureMessage); |
| |
| try { |
| Result.compareResults(r1, r2); |
| } catch (Exception e) { |
| fail(failureMessage); |
| } |
| } |
| |
| @Test |
| public void testReadPointAndPartialResults() throws Exception { |
| final TableName tableName = TableName.valueOf(name.getMethodName()); |
| int numRows = 5; |
| int numFamilies = 5; |
| int numQualifiers = 5; |
| byte[][] rows = HTestConst.makeNAscii(Bytes.toBytes("testRow"), numRows); |
| byte[][] families = HTestConst.makeNAscii(Bytes.toBytes("testFamily"), numFamilies); |
| byte[][] qualifiers = HTestConst.makeNAscii(Bytes.toBytes("testQualifier"), numQualifiers); |
| byte[] value = Bytes.createMaxByteArray(100); |
| |
| Table tmpTable = createTestTable(tableName, rows, families, qualifiers, value); |
| // Open scanner before deletes |
| ResultScanner scanner = |
| tmpTable.getScanner(new Scan().setMaxResultSize(1).setAllowPartialResults(true)); |
| // now the openScanner will also fetch data and will be executed lazily, i.e, only openScanner |
| // when you call next, so here we need to make a next call to open scanner. The maxResultSize |
| // limit can make sure that we will not fetch all the data at once, so the test sill works. |
| int scannerCount = scanner.next().rawCells().length; |
| Delete delete1 = new Delete(rows[0]); |
| delete1.addColumn(families[0], qualifiers[0], 0); |
| tmpTable.delete(delete1); |
| |
| Delete delete2 = new Delete(rows[1]); |
| delete2.addColumn(families[1], qualifiers[1], 1); |
| tmpTable.delete(delete2); |
| |
| // Should see all cells because scanner was opened prior to deletes |
| scannerCount += countCellsFromScanner(scanner); |
| int expectedCount = numRows * numFamilies * numQualifiers; |
| assertTrue("scannerCount: " + scannerCount + " expectedCount: " + expectedCount, |
| scannerCount == expectedCount); |
| |
| // Minus 2 for the two cells that were deleted |
| scanner = tmpTable.getScanner(new Scan().setMaxResultSize(1).setAllowPartialResults(true)); |
| scannerCount = countCellsFromScanner(scanner); |
| expectedCount = numRows * numFamilies * numQualifiers - 2; |
| assertTrue("scannerCount: " + scannerCount + " expectedCount: " + expectedCount, |
| scannerCount == expectedCount); |
| |
| scanner = tmpTable.getScanner(new Scan().setMaxResultSize(1).setAllowPartialResults(true)); |
| scannerCount = scanner.next().rawCells().length; |
| // Put in 2 new rows. The timestamps differ from the deleted rows |
| Put put1 = new Put(rows[0]); |
| put1.add(new KeyValue(rows[0], families[0], qualifiers[0], 1, value)); |
| tmpTable.put(put1); |
| |
| Put put2 = new Put(rows[1]); |
| put2.add(new KeyValue(rows[1], families[1], qualifiers[1], 2, value)); |
| tmpTable.put(put2); |
| |
| // Scanner opened prior to puts. Cell count shouldn't have changed |
| scannerCount += countCellsFromScanner(scanner); |
| expectedCount = numRows * numFamilies * numQualifiers - 2; |
| assertTrue("scannerCount: " + scannerCount + " expectedCount: " + expectedCount, |
| scannerCount == expectedCount); |
| |
| // Now the scanner should see the cells that were added by puts |
| scanner = tmpTable.getScanner(new Scan().setMaxResultSize(1).setAllowPartialResults(true)); |
| scannerCount = countCellsFromScanner(scanner); |
| expectedCount = numRows * numFamilies * numQualifiers; |
| assertTrue("scannerCount: " + scannerCount + " expectedCount: " + expectedCount, |
| scannerCount == expectedCount); |
| |
| TEST_UTIL.deleteTable(tableName); |
| } |
| |
| /** |
| * Exhausts the scanner by calling next repetitively. Once completely exhausted, close scanner and |
| * return total cell count |
| * @param scanner the scanner to exhaust |
| * @return the number of cells counted |
| * @throws Exception if there is a problem retrieving cells from the scanner |
| */ |
| private int countCellsFromScanner(ResultScanner scanner) throws Exception { |
| Result result = null; |
| int numCells = 0; |
| while ((result = scanner.next()) != null) { |
| numCells += result.rawCells().length; |
| } |
| |
| scanner.close(); |
| return numCells; |
| } |
| |
| /** |
| * Test partial Result re-assembly in the presence of different filters. The Results from the |
| * partial scanner should match the Results returned from a scanner that receives all of the |
| * results in one RPC to the server. The partial scanner is tested with a variety of different |
| * result sizes (all of which are less than the size necessary to fetch an entire row) |
| * @throws Exception |
| */ |
| @Test |
| public void testPartialResultsWithColumnFilter() throws Exception { |
| testPartialResultsWithColumnFilter(new FirstKeyOnlyFilter()); |
| testPartialResultsWithColumnFilter(new ColumnPrefixFilter(Bytes.toBytes("testQualifier5"))); |
| testPartialResultsWithColumnFilter(new ColumnRangeFilter(Bytes.toBytes("testQualifer1"), true, |
| Bytes.toBytes("testQualifier7"), true)); |
| |
| Set<byte[]> qualifiers = new LinkedHashSet<>(); |
| qualifiers.add(Bytes.toBytes("testQualifier5")); |
| testPartialResultsWithColumnFilter(new FirstKeyValueMatchingQualifiersFilter(qualifiers)); |
| } |
| |
| public void testPartialResultsWithColumnFilter(Filter filter) throws Exception { |
| assertTrue(!filter.hasFilterRow()); |
| |
| Scan partialScan = new Scan(); |
| partialScan.setFilter(filter); |
| |
| Scan oneshotScan = new Scan(); |
| oneshotScan.setFilter(filter); |
| oneshotScan.setMaxResultSize(Long.MAX_VALUE); |
| |
| for (int i = 1; i <= NUM_COLS; i++) { |
| partialScan.setMaxResultSize(getResultSizeForNumberOfCells(i)); |
| testEquivalenceOfScanResults(TABLE, partialScan, oneshotScan); |
| } |
| } |
| |
| private void moveRegion(Table table, int index) throws IOException{ |
| List<Pair<RegionInfo, ServerName>> regions = MetaTableAccessor |
| .getTableRegionsAndLocations(TEST_UTIL.getConnection(), |
| table.getName()); |
| assertEquals(1, regions.size()); |
| RegionInfo regionInfo = regions.get(0).getFirst(); |
| ServerName name = TEST_UTIL.getHBaseCluster().getRegionServer(index).getServerName(); |
| TEST_UTIL.getAdmin().move(regionInfo.getEncodedNameAsBytes(), name); |
| } |
| |
| private void assertCell(Cell cell, byte[] row, byte[] cf, byte[] cq) { |
| assertArrayEquals(row, |
| Bytes.copy(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength())); |
| assertArrayEquals(cf, |
| Bytes.copy(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength())); |
| assertArrayEquals(cq, |
| Bytes.copy(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength())); |
| } |
| |
| @Test |
| public void testPartialResultWhenRegionMove() throws IOException { |
| Table table = createTestTable(TableName.valueOf(name.getMethodName()), |
| ROWS, FAMILIES, QUALIFIERS, VALUE); |
| |
| moveRegion(table, 1); |
| |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(1); |
| scan.setAllowPartialResults(true); |
| ResultScanner scanner = table.getScanner(scan); |
| for (int i = 0; i < NUM_FAMILIES * NUM_QUALIFIERS - 1; i++) { |
| scanner.next(); |
| } |
| Result result1 = scanner.next(); |
| assertEquals(1, result1.rawCells().length); |
| Cell c1 = result1.rawCells()[0]; |
| assertCell(c1, ROWS[0], FAMILIES[NUM_FAMILIES - 1], QUALIFIERS[NUM_QUALIFIERS - 1]); |
| assertFalse(result1.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 2); |
| |
| Result result2 = scanner.next(); |
| assertEquals(1, result2.rawCells().length); |
| Cell c2 = result2.rawCells()[0]; |
| assertCell(c2, ROWS[1], FAMILIES[0], QUALIFIERS[0]); |
| assertTrue(result2.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 3); |
| |
| Result result3 = scanner.next(); |
| assertEquals(1, result3.rawCells().length); |
| Cell c3 = result3.rawCells()[0]; |
| assertCell(c3, ROWS[1], FAMILIES[0], QUALIFIERS[1]); |
| assertTrue(result3.mayHaveMoreCellsInRow()); |
| |
| } |
| |
| @Test |
| public void testReversedPartialResultWhenRegionMove() throws IOException { |
| Table table = createTestTable(TableName.valueOf(name.getMethodName()), |
| ROWS, FAMILIES, QUALIFIERS, VALUE); |
| |
| moveRegion(table, 1); |
| |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(1); |
| scan.setAllowPartialResults(true); |
| scan.setReversed(true); |
| ResultScanner scanner = table.getScanner(scan); |
| for (int i = 0; i < NUM_FAMILIES * NUM_QUALIFIERS-1; i++) { |
| scanner.next(); |
| } |
| Result result1 = scanner.next(); |
| assertEquals(1, result1.rawCells().length); |
| Cell c1 = result1.rawCells()[0]; |
| assertCell(c1, ROWS[NUM_ROWS-1], FAMILIES[NUM_FAMILIES - 1], QUALIFIERS[NUM_QUALIFIERS - 1]); |
| assertFalse(result1.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 2); |
| |
| Result result2 = scanner.next(); |
| assertEquals(1, result2.rawCells().length); |
| Cell c2 = result2.rawCells()[0]; |
| assertCell(c2, ROWS[NUM_ROWS-2], FAMILIES[0], QUALIFIERS[0]); |
| assertTrue(result2.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 3); |
| |
| Result result3 = scanner.next(); |
| assertEquals(1, result3.rawCells().length); |
| Cell c3 = result3.rawCells()[0]; |
| assertCell(c3, ROWS[NUM_ROWS-2], FAMILIES[0], QUALIFIERS[1]); |
| assertTrue(result3.mayHaveMoreCellsInRow()); |
| |
| } |
| |
| @Test |
| public void testCompleteResultWhenRegionMove() throws IOException { |
| Table table = createTestTable(TableName.valueOf(name.getMethodName()), |
| ROWS, FAMILIES, QUALIFIERS, VALUE); |
| |
| moveRegion(table, 1); |
| |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(1); |
| scan.setCaching(1); |
| ResultScanner scanner = table.getScanner(scan); |
| |
| Result result1 = scanner.next(); |
| assertEquals(NUM_FAMILIES * NUM_QUALIFIERS, result1.rawCells().length); |
| Cell c1 = result1.rawCells()[0]; |
| assertCell(c1, ROWS[0], FAMILIES[0], QUALIFIERS[0]); |
| assertFalse(result1.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 2); |
| |
| Result result2 = scanner.next(); |
| assertEquals(NUM_FAMILIES * NUM_QUALIFIERS, result2.rawCells().length); |
| Cell c2 = result2.rawCells()[0]; |
| assertCell(c2, ROWS[1], FAMILIES[0], QUALIFIERS[0]); |
| assertFalse(result2.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 3); |
| |
| Result result3 = scanner.next(); |
| assertEquals(NUM_FAMILIES * NUM_QUALIFIERS, result3.rawCells().length); |
| Cell c3 = result3.rawCells()[0]; |
| assertCell(c3, ROWS[2], FAMILIES[0], QUALIFIERS[0]); |
| assertFalse(result3.mayHaveMoreCellsInRow()); |
| |
| } |
| |
| @Test |
| public void testReversedCompleteResultWhenRegionMove() throws IOException { |
| Table table = createTestTable(TableName.valueOf(name.getMethodName()), |
| ROWS, FAMILIES, QUALIFIERS, VALUE); |
| |
| moveRegion(table, 1); |
| |
| Scan scan = new Scan(); |
| scan.setMaxResultSize(1); |
| scan.setCaching(1); |
| scan.setReversed(true); |
| ResultScanner scanner = table.getScanner(scan); |
| |
| Result result1 = scanner.next(); |
| assertEquals(NUM_FAMILIES*NUM_QUALIFIERS, result1.rawCells().length); |
| Cell c1 = result1.rawCells()[0]; |
| assertCell(c1, ROWS[NUM_ROWS-1], FAMILIES[0], QUALIFIERS[0]); |
| assertFalse(result1.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 2); |
| |
| Result result2 = scanner.next(); |
| assertEquals(NUM_FAMILIES*NUM_QUALIFIERS, result2.rawCells().length); |
| Cell c2 = result2.rawCells()[0]; |
| assertCell(c2, ROWS[NUM_ROWS-2], FAMILIES[0], QUALIFIERS[0]); |
| assertFalse(result2.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 3); |
| |
| Result result3 = scanner.next(); |
| assertEquals(NUM_FAMILIES*NUM_QUALIFIERS, result3.rawCells().length); |
| Cell c3 = result3.rawCells()[0]; |
| assertCell(c3, ROWS[NUM_ROWS-3], FAMILIES[0], QUALIFIERS[0]); |
| assertFalse(result3.mayHaveMoreCellsInRow()); |
| |
| } |
| |
| @Test |
| public void testBatchingResultWhenRegionMove() throws IOException { |
| // If user setBatch(5) and rpc returns 3+5+5+5+3 cells, |
| // we should return 5+5+5+5+1 to user. |
| // setBatch doesn't mean setAllowPartialResult(true) |
| Table table = createTestTable(TableName.valueOf(name.getMethodName()), ROWS, FAMILIES, |
| QUALIFIERS, VALUE); |
| |
| Put put = new Put(ROWS[1]); |
| put.addColumn(FAMILIES[0], QUALIFIERS[1], new byte[VALUE_SIZE * 10]); |
| table.put(put); |
| Delete delete = new Delete(ROWS[1]); |
| delete.addColumn(FAMILIES[NUM_FAMILIES - 1], QUALIFIERS[NUM_QUALIFIERS - 1]); |
| table.delete(delete); |
| |
| moveRegion(table, 1); |
| |
| Scan scan = new Scan(); |
| scan.setCaching(1); |
| scan.setBatch(5); |
| scan.setMaxResultSize(VALUE_SIZE * 6); |
| |
| ResultScanner scanner = table.getScanner(scan); |
| for (int i = 0; i < NUM_FAMILIES * NUM_QUALIFIERS / 5 - 1; i++) { |
| assertTrue(scanner.next().mayHaveMoreCellsInRow()); |
| } |
| Result result1 = scanner.next(); |
| assertEquals(5, result1.rawCells().length); |
| assertCell(result1.rawCells()[0], ROWS[0], FAMILIES[NUM_FAMILIES - 1], |
| QUALIFIERS[NUM_QUALIFIERS - 5]); |
| assertCell(result1.rawCells()[4], ROWS[0], FAMILIES[NUM_FAMILIES - 1], |
| QUALIFIERS[NUM_QUALIFIERS - 1]); |
| assertFalse(result1.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 2); |
| |
| Result result2 = scanner.next(); |
| assertEquals(5, result2.rawCells().length); |
| assertCell(result2.rawCells()[0], ROWS[1], FAMILIES[0], QUALIFIERS[0]); |
| assertCell(result2.rawCells()[4], ROWS[1], FAMILIES[0], QUALIFIERS[4]); |
| assertTrue(result2.mayHaveMoreCellsInRow()); |
| |
| moveRegion(table, 3); |
| |
| Result result3 = scanner.next(); |
| assertEquals(5, result3.rawCells().length); |
| assertCell(result3.rawCells()[0], ROWS[1], FAMILIES[0], QUALIFIERS[5]); |
| assertCell(result3.rawCells()[4], ROWS[1], FAMILIES[0], QUALIFIERS[9]); |
| assertTrue(result3.mayHaveMoreCellsInRow()); |
| |
| for (int i = 0; i < NUM_FAMILIES * NUM_QUALIFIERS / 5 - 3; i++) { |
| Result result = scanner.next(); |
| assertEquals(5, result.rawCells().length); |
| assertTrue(result.mayHaveMoreCellsInRow()); |
| } |
| Result result = scanner.next(); |
| assertEquals(4, result.rawCells().length); |
| assertFalse(result.mayHaveMoreCellsInRow()); |
| |
| |
| for (int i = 2; i < NUM_ROWS; i++) { |
| for (int j = 0; j < NUM_FAMILIES; j++) { |
| for (int k = 0; k < NUM_QUALIFIERS; k += 5) { |
| result = scanner.next(); |
| assertCell(result.rawCells()[0], ROWS[i], FAMILIES[j], QUALIFIERS[k]); |
| assertEquals(5, result.rawCells().length); |
| if (j == NUM_FAMILIES - 1 && k == NUM_QUALIFIERS - 5) { |
| assertFalse(result.mayHaveMoreCellsInRow()); |
| } else { |
| assertTrue(result.mayHaveMoreCellsInRow()); |
| } |
| } |
| } |
| } |
| assertNull(scanner.next()); |
| } |
| |
| @Test |
| public void testDontThrowUnknowScannerExceptionToClient() throws Exception { |
| Table table = createTestTable(TableName.valueOf(name.getMethodName()), ROWS, FAMILIES, |
| QUALIFIERS, VALUE); |
| Scan scan = new Scan(); |
| scan.setCaching(1); |
| ResultScanner scanner = table.getScanner(scan); |
| scanner.next(); |
| Thread.sleep(timeout * 2); |
| int count = 1; |
| while (scanner.next() != null) { |
| count++; |
| } |
| assertEquals(NUM_ROWS, count); |
| scanner.close(); |
| } |
| |
| @Test |
| public void testMayHaveMoreCellsInRowReturnsTrueAndSetBatch() throws IOException { |
| Table table = createTestTable(TableName.valueOf(name.getMethodName()), ROWS, FAMILIES, |
| QUALIFIERS, VALUE); |
| Scan scan = new Scan(); |
| scan.setBatch(1); |
| scan.setFilter(new FirstKeyOnlyFilter()); |
| ResultScanner scanner = table.getScanner(scan); |
| Result result; |
| while ((result = scanner.next()) != null) { |
| assertTrue(result.rawCells() != null); |
| assertEquals(1, result.rawCells().length); |
| } |
| } |
| |
| } |