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.regionserver;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertTrue;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Map;
032import java.util.Random;
033import java.util.Set;
034import java.util.TreeSet;
035import org.apache.commons.lang3.ArrayUtils;
036import org.apache.hadoop.hbase.Cell;
037import org.apache.hadoop.hbase.CellComparatorImpl;
038import org.apache.hadoop.hbase.CellUtil;
039import org.apache.hadoop.hbase.HBaseClassTestRule;
040import org.apache.hadoop.hbase.HBaseTestingUtility;
041import org.apache.hadoop.hbase.HColumnDescriptor;
042import org.apache.hadoop.hbase.KeyValue;
043import org.apache.hadoop.hbase.KeyValueTestUtil;
044import org.apache.hadoop.hbase.PrivateCellUtil;
045import org.apache.hadoop.hbase.client.Delete;
046import org.apache.hadoop.hbase.client.Put;
047import org.apache.hadoop.hbase.client.Scan;
048import org.apache.hadoop.hbase.io.compress.Compression;
049import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
050import org.apache.hadoop.hbase.testclassification.MediumTests;
051import org.apache.hadoop.hbase.testclassification.RegionServerTests;
052import org.apache.hadoop.hbase.util.Bytes;
053import org.junit.ClassRule;
054import org.junit.Test;
055import org.junit.experimental.categories.Category;
056import org.junit.runner.RunWith;
057import org.junit.runners.Parameterized;
058import org.junit.runners.Parameterized.Parameters;
059import org.slf4j.Logger;
060import org.slf4j.LoggerFactory;
061
062/**
063 * Tests optimized scanning of multiple columns.
064 */
065@RunWith(Parameterized.class)
066@Category({RegionServerTests.class, MediumTests.class})
067public class TestMultiColumnScanner {
068
069  @ClassRule
070  public static final HBaseClassTestRule CLASS_RULE =
071      HBaseClassTestRule.forClass(TestMultiColumnScanner.class);
072
073  private static final Logger LOG = LoggerFactory.getLogger(TestMultiColumnScanner.class);
074
075  private static final String TABLE_NAME =
076      TestMultiColumnScanner.class.getSimpleName();
077
078  static final int MAX_VERSIONS = 50;
079
080  private static final String FAMILY = "CF";
081  private static final byte[] FAMILY_BYTES = Bytes.toBytes(FAMILY);
082
083  /**
084   * The size of the column qualifier set used. Increasing this parameter
085   * exponentially increases test time.
086   */
087  private static final int NUM_COLUMNS = 8;
088
089  private static final int MAX_COLUMN_BIT_MASK = 1 << NUM_COLUMNS - 1;
090  private static final int NUM_FLUSHES = 10;
091  private static final int NUM_ROWS = 20;
092
093  /** A large value of type long for use as a timestamp */
094  private static final long BIG_LONG = 9111222333444555666L;
095
096  /**
097   * Timestamps to test with. Cannot use {@link Long#MAX_VALUE} here, because
098   * it will be replaced by an timestamp auto-generated based on the time.
099   */
100  private static final long[] TIMESTAMPS = new long[] { 1, 3, 5,
101      Integer.MAX_VALUE, BIG_LONG, Long.MAX_VALUE - 1 };
102
103  /** The probability that a column is skipped in a store file. */
104  private static final double COLUMN_SKIP_IN_STORE_FILE_PROB = 0.7;
105
106  /** The probability of skipping a column in a single row */
107  private static final double COLUMN_SKIP_IN_ROW_PROB = 0.1;
108
109  /** The probability of skipping a column everywhere */
110  private static final double COLUMN_SKIP_EVERYWHERE_PROB = 0.1;
111
112  /** The probability to delete a row/column pair */
113  private static final double DELETE_PROBABILITY = 0.02;
114
115  private final static HBaseTestingUtility TEST_UTIL = HBaseTestingUtility.createLocalHTU();
116
117  private final Compression.Algorithm comprAlgo;
118  private final BloomType bloomType;
119  private final DataBlockEncoding dataBlockEncoding;
120
121  // Some static sanity-checking.
122  static {
123    assertTrue(BIG_LONG > 0.9 * Long.MAX_VALUE); // Guard against typos.
124
125    // Ensure TIMESTAMPS are sorted.
126    for (int i = 0; i < TIMESTAMPS.length - 1; ++i)
127      assertTrue(TIMESTAMPS[i] < TIMESTAMPS[i + 1]);
128  }
129
130  @Parameters
131  public static final Collection<Object[]> parameters() {
132    List<Object[]> parameters = new ArrayList<>();
133    for (Object[] bloomAndCompressionParams :
134        HBaseTestingUtility.BLOOM_AND_COMPRESSION_COMBINATIONS) {
135      for (boolean useDataBlockEncoding : new boolean[]{false, true}) {
136        parameters.add(ArrayUtils.add(bloomAndCompressionParams,
137            useDataBlockEncoding));
138      }
139    }
140    return parameters;
141  }
142
143  public TestMultiColumnScanner(Compression.Algorithm comprAlgo,
144      BloomType bloomType, boolean useDataBlockEncoding) {
145    this.comprAlgo = comprAlgo;
146    this.bloomType = bloomType;
147    this.dataBlockEncoding = useDataBlockEncoding ? DataBlockEncoding.PREFIX :
148        DataBlockEncoding.NONE;
149  }
150
151  @Test
152  public void testMultiColumnScanner() throws IOException {
153    HRegion region = TEST_UTIL.createTestRegion(TABLE_NAME,
154        new HColumnDescriptor(FAMILY)
155            .setCompressionType(comprAlgo)
156            .setBloomFilterType(bloomType)
157            .setMaxVersions(MAX_VERSIONS)
158            .setDataBlockEncoding(dataBlockEncoding)
159    );
160    List<String> rows = sequentialStrings("row", NUM_ROWS);
161    List<String> qualifiers = sequentialStrings("qual", NUM_COLUMNS);
162    List<KeyValue> kvs = new ArrayList<>();
163    Set<String> keySet = new HashSet<>();
164
165    // A map from <row>_<qualifier> to the most recent delete timestamp for
166    // that column.
167    Map<String, Long> lastDelTimeMap = new HashMap<>();
168
169    Random rand = new Random(29372937L);
170    Set<String> rowQualSkip = new HashSet<>();
171
172    // Skip some columns in some rows. We need to test scanning over a set
173    // of columns when some of the columns are not there.
174    for (String row : rows)
175      for (String qual : qualifiers)
176        if (rand.nextDouble() < COLUMN_SKIP_IN_ROW_PROB) {
177          LOG.info("Skipping " + qual + " in row " + row);
178          rowQualSkip.add(rowQualKey(row, qual));
179        }
180
181    // Also skip some columns in all rows.
182    for (String qual : qualifiers)
183      if (rand.nextDouble() < COLUMN_SKIP_EVERYWHERE_PROB) {
184        LOG.info("Skipping " + qual + " in all rows");
185        for (String row : rows)
186          rowQualSkip.add(rowQualKey(row, qual));
187      }
188
189    for (int iFlush = 0; iFlush < NUM_FLUSHES; ++iFlush) {
190      for (String qual : qualifiers) {
191        // This is where we decide to include or not include this column into
192        // this store file, regardless of row and timestamp.
193        if (rand.nextDouble() < COLUMN_SKIP_IN_STORE_FILE_PROB)
194          continue;
195
196        byte[] qualBytes = Bytes.toBytes(qual);
197        for (String row : rows) {
198          Put p = new Put(Bytes.toBytes(row));
199          for (long ts : TIMESTAMPS) {
200            String value = createValue(row, qual, ts);
201            KeyValue kv = KeyValueTestUtil.create(row, FAMILY, qual, ts,
202                value);
203            assertEquals(kv.getTimestamp(), ts);
204            p.add(kv);
205            String keyAsString = kv.toString();
206            if (!keySet.contains(keyAsString)) {
207              keySet.add(keyAsString);
208              kvs.add(kv);
209            }
210          }
211          region.put(p);
212
213          Delete d = new Delete(Bytes.toBytes(row));
214          boolean deletedSomething = false;
215          for (long ts : TIMESTAMPS)
216            if (rand.nextDouble() < DELETE_PROBABILITY) {
217              d.addColumns(FAMILY_BYTES, qualBytes, ts);
218              String rowAndQual = row + "_" + qual;
219              Long whenDeleted = lastDelTimeMap.get(rowAndQual);
220              lastDelTimeMap.put(rowAndQual, whenDeleted == null ? ts
221                  : Math.max(ts, whenDeleted));
222              deletedSomething = true;
223            }
224          if (deletedSomething)
225            region.delete(d);
226        }
227      }
228      region.flush(true);
229    }
230
231    Collections.sort(kvs, CellComparatorImpl.COMPARATOR);
232    for (int maxVersions = 1; maxVersions <= TIMESTAMPS.length; ++maxVersions) {
233      for (int columnBitMask = 1; columnBitMask <= MAX_COLUMN_BIT_MASK; ++columnBitMask) {
234        Scan scan = new Scan();
235        scan.setMaxVersions(maxVersions);
236        Set<String> qualSet = new TreeSet<>();
237        {
238          int columnMaskTmp = columnBitMask;
239          for (String qual : qualifiers) {
240            if ((columnMaskTmp & 1) != 0) {
241              scan.addColumn(FAMILY_BYTES, Bytes.toBytes(qual));
242              qualSet.add(qual);
243            }
244            columnMaskTmp >>= 1;
245          }
246          assertEquals(0, columnMaskTmp);
247        }
248
249        InternalScanner scanner = region.getScanner(scan);
250        List<Cell> results = new ArrayList<>();
251
252        int kvPos = 0;
253        int numResults = 0;
254        String queryInfo = "columns queried: " + qualSet + " (columnBitMask="
255            + columnBitMask + "), maxVersions=" + maxVersions;
256
257        while (scanner.next(results) || results.size() > 0) {
258          for (Cell kv : results) {
259            while (kvPos < kvs.size()
260                && !matchesQuery(kvs.get(kvPos), qualSet, maxVersions,
261                    lastDelTimeMap)) {
262              ++kvPos;
263            }
264            String rowQual = getRowQualStr(kv);
265            String deleteInfo = "";
266            Long lastDelTS = lastDelTimeMap.get(rowQual);
267            if (lastDelTS != null) {
268              deleteInfo = "; last timestamp when row/column " + rowQual
269                  + " was deleted: " + lastDelTS;
270            }
271            assertTrue("Scanner returned additional key/value: " + kv + ", "
272                + queryInfo + deleteInfo + ";", kvPos < kvs.size());
273            assertTrue("Scanner returned wrong key/value; " + queryInfo + deleteInfo + ";",
274              PrivateCellUtil.equalsIgnoreMvccVersion(kvs.get(kvPos), (kv)));
275            ++kvPos;
276            ++numResults;
277          }
278          results.clear();
279        }
280        for (; kvPos < kvs.size(); ++kvPos) {
281          KeyValue remainingKV = kvs.get(kvPos);
282          assertFalse("Matching column not returned by scanner: "
283              + remainingKV + ", " + queryInfo + ", results returned: "
284              + numResults, matchesQuery(remainingKV, qualSet, maxVersions,
285              lastDelTimeMap));
286        }
287      }
288    }
289    assertTrue("This test is supposed to delete at least some row/column " +
290        "pairs", lastDelTimeMap.size() > 0);
291    LOG.info("Number of row/col pairs deleted at least once: " +
292       lastDelTimeMap.size());
293    HBaseTestingUtility.closeRegionAndWAL(region);
294  }
295
296  private static String getRowQualStr(Cell kv) {
297    String rowStr = Bytes.toString(CellUtil.cloneRow(kv));
298    String qualStr = Bytes.toString(CellUtil.cloneQualifier(kv));
299    return rowStr + "_" + qualStr;
300  }
301
302  private static boolean matchesQuery(KeyValue kv, Set<String> qualSet,
303      int maxVersions, Map<String, Long> lastDelTimeMap) {
304    Long lastDelTS = lastDelTimeMap.get(getRowQualStr(kv));
305    long ts = kv.getTimestamp();
306    return qualSet.contains(qualStr(kv))
307        && ts >= TIMESTAMPS[TIMESTAMPS.length - maxVersions]
308        && (lastDelTS == null || ts > lastDelTS);
309  }
310
311  private static String qualStr(KeyValue kv) {
312    return Bytes.toString(kv.getQualifierArray(), kv.getQualifierOffset(),
313        kv.getQualifierLength());
314  }
315
316  private static String rowQualKey(String row, String qual) {
317    return row + "_" + qual;
318  }
319
320  static String createValue(String row, String qual, long ts) {
321    return "value_for_" + row + "_" + qual + "_" + ts;
322  }
323
324  private static List<String> sequentialStrings(String prefix, int n) {
325    List<String> lst = new ArrayList<>();
326    for (int i = 0; i < n; ++i) {
327      StringBuilder sb = new StringBuilder();
328      sb.append(prefix + i);
329
330      // Make column length depend on i.
331      int iBitShifted = i;
332      while (iBitShifted != 0) {
333        sb.append((iBitShifted & 1) == 0 ? 'a' : 'b');
334        iBitShifted >>= 1;
335      }
336
337      lst.add(sb.toString());
338    }
339
340    return lst;
341  }
342
343
344}
345