001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertTrue; 022 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.List; 026import org.apache.hadoop.hbase.client.Put; 027import org.apache.hadoop.hbase.client.Result; 028import org.apache.hadoop.hbase.client.ResultScanner; 029import org.apache.hadoop.hbase.client.Scan; 030import org.apache.hadoop.hbase.client.Table; 031import org.apache.hadoop.hbase.client.metrics.ScanMetrics; 032import org.apache.hadoop.hbase.client.metrics.ServerSideScanMetrics; 033import org.apache.hadoop.hbase.filter.BinaryComparator; 034import org.apache.hadoop.hbase.filter.ColumnPrefixFilter; 035import org.apache.hadoop.hbase.filter.Filter; 036import org.apache.hadoop.hbase.filter.FilterList; 037import org.apache.hadoop.hbase.filter.FilterList.Operator; 038import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter; 039import org.apache.hadoop.hbase.filter.RowFilter; 040import org.apache.hadoop.hbase.filter.SingleColumnValueExcludeFilter; 041import org.apache.hadoop.hbase.filter.SingleColumnValueFilter; 042import org.apache.hadoop.hbase.testclassification.LargeTests; 043import org.apache.hadoop.hbase.util.Bytes; 044import org.junit.AfterClass; 045import org.junit.BeforeClass; 046import org.junit.ClassRule; 047import org.junit.Test; 048import org.junit.experimental.categories.Category; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052@Category(LargeTests.class) 053public class TestServerSideScanMetricsFromClientSide { 054 private static final Logger LOG = 055 LoggerFactory.getLogger(TestServerSideScanMetricsFromClientSide.class); 056 057 @ClassRule 058 public static final HBaseClassTestRule CLASS_RULE = 059 HBaseClassTestRule.forClass(TestServerSideScanMetricsFromClientSide.class); 060 061 private final static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); 062 063 private static Table TABLE = null; 064 065 /** 066 * Table configuration 067 */ 068 private static TableName TABLE_NAME = TableName.valueOf("testTable"); 069 070 private static int NUM_ROWS = 10; 071 private static byte[] ROW = Bytes.toBytes("testRow"); 072 private static byte[][] ROWS = HTestConst.makeNAscii(ROW, NUM_ROWS); 073 074 // Should keep this value below 10 to keep generation of expected kv's simple. If above 10 then 075 // table/row/cf1/... will be followed by table/row/cf10/... instead of table/row/cf2/... which 076 // breaks the simple generation of expected kv's 077 private static int NUM_FAMILIES = 1; 078 private static byte[] FAMILY = Bytes.toBytes("testFamily"); 079 private static byte[][] FAMILIES = HTestConst.makeNAscii(FAMILY, NUM_FAMILIES); 080 081 private static int NUM_QUALIFIERS = 1; 082 private static byte[] QUALIFIER = Bytes.toBytes("testQualifier"); 083 private static byte[][] QUALIFIERS = HTestConst.makeNAscii(QUALIFIER, NUM_QUALIFIERS); 084 085 private static int VALUE_SIZE = 10; 086 private static byte[] VALUE = Bytes.createMaxByteArray(VALUE_SIZE); 087 088 private static int NUM_COLS = NUM_FAMILIES * NUM_QUALIFIERS; 089 090 // Approximation of how large the heap size of cells in our table. Should be accessed through 091 // getCellHeapSize(). 092 private static long CELL_HEAP_SIZE = -1; 093 094 @BeforeClass 095 public static void setUpBeforeClass() throws Exception { 096 TEST_UTIL.startMiniCluster(3); 097 TABLE = createTestTable(TABLE_NAME, ROWS, FAMILIES, QUALIFIERS, VALUE); 098 } 099 100 static Table createTestTable(TableName name, byte[][] rows, byte[][] families, 101 byte[][] qualifiers, byte[] cellValue) throws IOException { 102 Table ht = TEST_UTIL.createTable(name, families); 103 List<Put> puts = createPuts(rows, families, qualifiers, cellValue); 104 ht.put(puts); 105 106 return ht; 107 } 108 109 @AfterClass 110 public static void tearDownAfterClass() throws Exception { 111 TEST_UTIL.shutdownMiniCluster(); 112 } 113 114 /** 115 * Make puts to put the input value into each combination of row, family, and qualifier 116 * @param rows the rows to use 117 * @param families the column families to use 118 * @param qualifiers the column qualifiers to use 119 * @param value the value to put 120 * @return the putted input values added in puts 121 * @throws IOException If an IO problem is encountered 122 */ 123 static ArrayList<Put> createPuts(byte[][] rows, byte[][] families, byte[][] qualifiers, 124 byte[] value) throws IOException { 125 Put put; 126 ArrayList<Put> puts = new ArrayList<>(); 127 128 for (int row = 0; row < rows.length; row++) { 129 put = new Put(rows[row]); 130 for (int fam = 0; fam < families.length; fam++) { 131 for (int qual = 0; qual < qualifiers.length; qual++) { 132 KeyValue kv = new KeyValue(rows[row], families[fam], qualifiers[qual], qual, value); 133 put.add(kv); 134 } 135 } 136 puts.add(put); 137 } 138 139 return puts; 140 } 141 142 /** 143 * @return The approximate heap size of a cell in the test table. All cells should have 144 * approximately the same heap size, so the value is cached to avoid repeating the 145 * calculation 146 * @throws Exception on unexpected failure 147 */ 148 private long getCellHeapSize() throws Exception { 149 if (CELL_HEAP_SIZE == -1) { 150 // Do a partial scan that will return a single result with a single cell 151 Scan scan = new Scan(); 152 scan.setMaxResultSize(1); 153 scan.setAllowPartialResults(true); 154 ResultScanner scanner = TABLE.getScanner(scan); 155 156 Result result = scanner.next(); 157 158 assertTrue(result != null); 159 assertTrue(result.rawCells() != null); 160 assertTrue(result.rawCells().length == 1); 161 162 CELL_HEAP_SIZE = result.rawCells()[0].heapSize(); 163 scanner.close(); 164 } 165 166 return CELL_HEAP_SIZE; 167 } 168 169 @Test 170 public void testRowsSeenMetricWithSync() throws Exception { 171 testRowsSeenMetric(false); 172 } 173 174 @Test 175 public void testRowsSeenMetricWithAsync() throws Exception { 176 testRowsSeenMetric(true); 177 } 178 179 private void testRowsSeenMetric(boolean async) throws Exception { 180 // Base scan configuration 181 Scan baseScan; 182 baseScan = new Scan(); 183 baseScan.setScanMetricsEnabled(true); 184 baseScan.setAsyncPrefetch(async); 185 try { 186 testRowsSeenMetric(baseScan); 187 188 // Test case that only a single result will be returned per RPC to the serer 189 baseScan.setCaching(1); 190 testRowsSeenMetric(baseScan); 191 192 // Test case that partial results are returned from the server. At most one cell will be 193 // contained in each response 194 baseScan.setMaxResultSize(1); 195 testRowsSeenMetric(baseScan); 196 197 // Test case that size limit is set such that a few cells are returned per partial result from 198 // the server 199 baseScan.setCaching(NUM_ROWS); 200 baseScan.setMaxResultSize(getCellHeapSize() * (NUM_COLS - 1)); 201 testRowsSeenMetric(baseScan); 202 } catch (Throwable t) { 203 LOG.error("FAIL", t); 204 throw t; 205 } 206 } 207 208 public void testRowsSeenMetric(Scan baseScan) throws Exception { 209 Scan scan; 210 scan = new Scan(baseScan); 211 testMetric(scan, ServerSideScanMetrics.COUNT_OF_ROWS_SCANNED_KEY_METRIC_NAME, NUM_ROWS); 212 213 for (int i = 0; i < ROWS.length - 1; i++) { 214 scan = new Scan(baseScan); 215 scan.withStartRow(ROWS[0]); 216 scan.withStopRow(ROWS[i + 1]); 217 testMetric(scan, ServerSideScanMetrics.COUNT_OF_ROWS_SCANNED_KEY_METRIC_NAME, i + 1); 218 } 219 220 for (int i = ROWS.length - 1; i > 0; i--) { 221 scan = new Scan(baseScan); 222 scan.withStartRow(ROWS[i - 1]); 223 scan.withStopRow(ROWS[ROWS.length - 1]); 224 testMetric(scan, ServerSideScanMetrics.COUNT_OF_ROWS_SCANNED_KEY_METRIC_NAME, 225 ROWS.length - i); 226 } 227 228 // The filter should filter out all rows, but we still expect to see every row. 229 Filter filter = 230 new RowFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("xyz"))); 231 scan = new Scan(baseScan); 232 scan.setFilter(filter); 233 testMetric(scan, ServerSideScanMetrics.COUNT_OF_ROWS_SCANNED_KEY_METRIC_NAME, ROWS.length); 234 235 // Filter should pass on all rows 236 SingleColumnValueFilter singleColumnValueFilter = 237 new SingleColumnValueFilter(FAMILIES[0], QUALIFIERS[0], CompareOperator.EQUAL, VALUE); 238 scan = new Scan(baseScan); 239 scan.setFilter(singleColumnValueFilter); 240 testMetric(scan, ServerSideScanMetrics.COUNT_OF_ROWS_SCANNED_KEY_METRIC_NAME, ROWS.length); 241 242 // Filter should filter out all rows 243 singleColumnValueFilter = 244 new SingleColumnValueFilter(FAMILIES[0], QUALIFIERS[0], CompareOperator.NOT_EQUAL, VALUE); 245 scan = new Scan(baseScan); 246 scan.setFilter(singleColumnValueFilter); 247 testMetric(scan, ServerSideScanMetrics.COUNT_OF_ROWS_SCANNED_KEY_METRIC_NAME, ROWS.length); 248 } 249 250 @Test 251 public void testRowsFilteredMetric() throws Exception { 252 // Base scan configuration 253 Scan baseScan; 254 baseScan = new Scan(); 255 baseScan.setScanMetricsEnabled(true); 256 257 // Test case where scan uses default values 258 testRowsFilteredMetric(baseScan); 259 260 // Test case where at most one Result is retrieved per RPC 261 baseScan.setCaching(1); 262 testRowsFilteredMetric(baseScan); 263 264 // Test case where size limit is very restrictive and partial results will be returned from 265 // server 266 baseScan.setMaxResultSize(1); 267 testRowsFilteredMetric(baseScan); 268 269 // Test a case where max result size limits response from server to only a few cells (not all 270 // cells from the row) 271 baseScan.setCaching(NUM_ROWS); 272 baseScan.setMaxResultSize(getCellHeapSize() * (NUM_COLS - 1)); 273 testRowsSeenMetric(baseScan); 274 } 275 276 public void testRowsFilteredMetric(Scan baseScan) throws Exception { 277 testRowsFilteredMetric(baseScan, null, 0); 278 279 // Row filter doesn't match any row key. All rows should be filtered 280 Filter filter = 281 new RowFilter(CompareOperator.EQUAL, new BinaryComparator(Bytes.toBytes("xyz"))); 282 testRowsFilteredMetric(baseScan, filter, ROWS.length); 283 284 // Filter will return results containing only the first key. Number of entire rows filtered 285 // should be 0. 286 filter = new FirstKeyOnlyFilter(); 287 testRowsFilteredMetric(baseScan, filter, 0); 288 289 // Column prefix will find some matching qualifier on each row. Number of entire rows filtered 290 // should be 0 291 filter = new ColumnPrefixFilter(QUALIFIERS[0]); 292 testRowsFilteredMetric(baseScan, filter, 0); 293 294 // Column prefix will NOT find any matching qualifier on any row. All rows should be filtered 295 filter = new ColumnPrefixFilter(Bytes.toBytes("xyz")); 296 testRowsFilteredMetric(baseScan, filter, ROWS.length); 297 298 // Matching column value should exist in each row. No rows should be filtered. 299 filter = new SingleColumnValueFilter(FAMILIES[0], QUALIFIERS[0], CompareOperator.EQUAL, VALUE); 300 testRowsFilteredMetric(baseScan, filter, 0); 301 302 // No matching column value should exist in any row. Filter all rows 303 filter = 304 new SingleColumnValueFilter(FAMILIES[0], QUALIFIERS[0], CompareOperator.NOT_EQUAL, VALUE); 305 testRowsFilteredMetric(baseScan, filter, ROWS.length); 306 307 List<Filter> filters = new ArrayList<>(); 308 filters.add(new RowFilter(CompareOperator.EQUAL, new BinaryComparator(ROWS[0]))); 309 filters.add(new RowFilter(CompareOperator.EQUAL, new BinaryComparator(ROWS[3]))); 310 int numberOfMatchingRowFilters = filters.size(); 311 filter = new FilterList(Operator.MUST_PASS_ONE, filters); 312 testRowsFilteredMetric(baseScan, filter, ROWS.length - numberOfMatchingRowFilters); 313 filters.clear(); 314 315 // Add a single column value exclude filter for each column... The net effect is that all 316 // columns will be excluded when scanning on the server side. This will result in an empty cell 317 // array in RegionScanner#nextInternal which should be interpreted as a row being filtered. 318 for (int family = 0; family < FAMILIES.length; family++) { 319 for (int qualifier = 0; qualifier < QUALIFIERS.length; qualifier++) { 320 filters.add(new SingleColumnValueExcludeFilter(FAMILIES[family], QUALIFIERS[qualifier], 321 CompareOperator.EQUAL, VALUE)); 322 } 323 } 324 filter = new FilterList(Operator.MUST_PASS_ONE, filters); 325 testRowsFilteredMetric(baseScan, filter, ROWS.length); 326 } 327 328 public void testRowsFilteredMetric(Scan baseScan, Filter filter, int expectedNumFiltered) 329 throws Exception { 330 Scan scan = new Scan(baseScan); 331 if (filter != null) { 332 scan.setFilter(filter); 333 } 334 testMetric(scan, ServerSideScanMetrics.COUNT_OF_ROWS_FILTERED_KEY_METRIC_NAME, 335 expectedNumFiltered); 336 } 337 338 /** 339 * Run the scan to completion and check the metric against the specified value 340 * @param scan The scan instance to use to record metrics 341 * @param metricKey The metric key name 342 * @param expectedValue The expected value of metric 343 * @throws Exception on unexpected failure 344 */ 345 public void testMetric(Scan scan, String metricKey, long expectedValue) throws Exception { 346 assertTrue("Scan should be configured to record metrics", scan.isScanMetricsEnabled()); 347 ResultScanner scanner = TABLE.getScanner(scan); 348 // Iterate through all the results 349 while (scanner.next() != null) { 350 continue; 351 } 352 scanner.close(); 353 ScanMetrics metrics = scanner.getScanMetrics(); 354 assertTrue("Metrics are null", metrics != null); 355 assertTrue("Metric : " + metricKey + " does not exist", metrics.hasCounter(metricKey)); 356 final long actualMetricValue = metrics.getCounter(metricKey).get(); 357 assertEquals( 358 "Metric: " + metricKey + " Expected: " + expectedValue + " Actual: " + actualMetricValue, 359 expectedValue, actualMetricValue); 360 361 } 362}