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}