Author: stack
Date: Fri Sep  9 05:08:18 2011
New Revision: 1167005

URL: http://svn.apache.org/viewvc?rev=1167005&view=rev
Log:
HBASE-4350 Fix a Bloom filter bug introduced by HFile v2 and 
TestMultiColumnScanner that caught it

Added:
    
hbase/trunk/src/test/java/org/apache/hadoop/hbase/regionserver/TestMultiColumnScanner.java

Added: 
hbase/trunk/src/test/java/org/apache/hadoop/hbase/regionserver/TestMultiColumnScanner.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/test/java/org/apache/hadoop/hbase/regionserver/TestMultiColumnScanner.java?rev=1167005&view=auto
==============================================================================
--- 
hbase/trunk/src/test/java/org/apache/hadoop/hbase/regionserver/TestMultiColumnScanner.java
 (added)
+++ 
hbase/trunk/src/test/java/org/apache/hadoop/hbase/regionserver/TestMultiColumnScanner.java
 Fri Sep  9 05:08:18 2011
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2010 The Apache Software Foundation
+ *
+ * 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.regionserver;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.HBaseTestingUtility;
+import org.apache.hadoop.hbase.HColumnDescriptor;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.HTableDescriptor;
+import org.apache.hadoop.hbase.KeyValue;
+import org.apache.hadoop.hbase.KeyValueTestUtil;
+import org.apache.hadoop.hbase.client.Delete;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Scan;
+import org.apache.hadoop.hbase.io.hfile.Compression;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests optimized scanning of multiple columns.
+ */
+@RunWith(Parameterized.class)
+public class TestMultiColumnScanner {
+
+  private static final Log LOG = 
LogFactory.getLog(TestMultiColumnScanner.class);
+
+  private static final String FAMILY = "CF";
+  private static final byte[] FAMILY_BYTES = Bytes.toBytes(FAMILY);
+  private static final int MAX_VERSIONS = 50;
+
+  /**
+   * The size of the column qualifier set used. Increasing this parameter
+   * exponentially increases test time.
+   */
+  private static final int NUM_COLUMNS = 8;
+
+  private static final int MAX_COLUMN_BIT_MASK = 1 << NUM_COLUMNS - 1;
+  private static final int NUM_FLUSHES = 10;
+  private static final int NUM_ROWS = 20;
+
+  /** A large value of type long for use as a timestamp */
+  private static final long BIG_LONG = 9111222333444555666L;
+
+  /**
+   * Timestamps to test with. Cannot use {@link Long#MAX_VALUE} here, because
+   * it will be replaced by an timestamp auto-generated based on the time.
+   */
+  private static final long[] TIMESTAMPS = new long[] { 1, 3, 5,
+      Integer.MAX_VALUE, BIG_LONG, Long.MAX_VALUE - 1 };
+
+  /** The probability that a column is skipped in a store file. */
+  private static final double COLUMN_SKIP_PROBABILITY = 0.7;
+
+  /** The probability to delete a row/column pair */
+  private static final double DELETE_PROBABILITY = 0.02;
+
+  private final static HBaseTestingUtility TEST_UTIL = new 
HBaseTestingUtility();
+
+  private Compression.Algorithm comprAlgo;
+  private StoreFile.BloomType bloomType;
+
+  // Some static sanity-checking.
+  static {
+    assertTrue(BIG_LONG > 0.9 * Long.MAX_VALUE); // Guard against typos.
+
+    // Ensure TIMESTAMPS are sorted.
+    for (int i = 0; i < TIMESTAMPS.length - 1; ++i)
+      assertTrue(TIMESTAMPS[i] < TIMESTAMPS[i + 1]);
+  }
+
+  @Parameters
+  public static final Collection<Object[]> parameters() {
+    List<Object[]> configurations = new ArrayList<Object[]>();
+    for (Compression.Algorithm comprAlgo : 
HBaseTestingUtility.COMPRESSION_ALGORITHMS) {
+      for (StoreFile.BloomType bloomType : StoreFile.BloomType.values()) {
+        configurations.add(new Object[] { comprAlgo, bloomType });
+      }
+    }
+    return configurations;
+  }
+
+  public TestMultiColumnScanner(Compression.Algorithm comprAlgo,
+      StoreFile.BloomType bloomType) {
+    this.comprAlgo = comprAlgo;
+    this.bloomType = bloomType;
+  }
+
+  @Test
+  public void testMultiColumnScanner() throws IOException {
+    String table = "TestMultiColumnScanner";
+    HColumnDescriptor hcd = new HColumnDescriptor(FAMILY_BYTES, MAX_VERSIONS,
+        comprAlgo.getName(), HColumnDescriptor.DEFAULT_IN_MEMORY,
+        HColumnDescriptor.DEFAULT_BLOCKCACHE, HColumnDescriptor.DEFAULT_TTL,
+        bloomType.toString());
+    HTableDescriptor htd = new HTableDescriptor(table);
+    htd.addFamily(hcd);
+    HRegionInfo info = new HRegionInfo(Bytes.toBytes(table), null, null, 
false);
+    HRegion region = HRegion.createHRegion(info,
+        HBaseTestingUtility.getTestDir(), TEST_UTIL.getConfiguration(), htd);
+    List<String> rows = sequentialStrings("row", NUM_ROWS);
+    List<String> qualifiers = sequentialStrings("qual", NUM_COLUMNS);
+    List<KeyValue> kvs = new ArrayList<KeyValue>();
+    Set<String> keySet = new HashSet<String>();
+
+    // A map from <row>_<qualifier> to the most recent delete timestamp for
+    // that column.
+    Map<String, Long> lastDelTimeMap = new HashMap<String, Long>();
+
+    Random rand = new Random(29372937L);
+    for (int iFlush = 0; iFlush < NUM_FLUSHES; ++iFlush) {
+      for (String qual : qualifiers) {
+        // This is where we decide to include or not include this column into
+        // this store file, regardless of row and timestamp.
+        if (rand.nextDouble() < COLUMN_SKIP_PROBABILITY)
+          continue;
+
+        byte[] qualBytes = Bytes.toBytes(qual);
+        for (String row : rows) {
+          Put p = new Put(Bytes.toBytes(row));
+          for (long ts : TIMESTAMPS) {
+            String value = createValue(row, qual, ts);
+            KeyValue kv = KeyValueTestUtil.create(row, FAMILY, qual, ts, 
value);
+            assertEquals(kv.getTimestamp(), ts);
+            p.add(kv);
+            String keyAsString = kv.toString();
+            if (!keySet.contains(keyAsString)) {
+              keySet.add(keyAsString);
+              kvs.add(kv);
+            }
+          }
+          region.put(p);
+
+          Delete d = new Delete(Bytes.toBytes(row));
+          boolean deletedSomething = false;
+          for (long ts : TIMESTAMPS)
+            if (rand.nextDouble() < DELETE_PROBABILITY) {
+              d.deleteColumns(FAMILY_BYTES, qualBytes, ts);
+              String rowAndQual = row + "_" + qual;
+              Long whenDeleted = lastDelTimeMap.get(rowAndQual);
+              lastDelTimeMap.put(rowAndQual, whenDeleted == null ? ts
+                  : Math.max(ts, whenDeleted));
+              deletedSomething = true;
+            }
+          if (deletedSomething)
+            region.delete(d, null, true);
+        }
+      }
+      region.flushcache();
+    }
+
+    Collections.sort(kvs, KeyValue.COMPARATOR);
+    for (int maxVersions = 1; maxVersions <= TIMESTAMPS.length; ++maxVersions) 
{
+      for (int columnBitMask = 1; columnBitMask <= MAX_COLUMN_BIT_MASK; 
++columnBitMask) {
+        Scan scan = new Scan();
+        scan.setMaxVersions(maxVersions);
+        Set<String> qualSet = new TreeSet<String>();
+        {
+          int columnMaskTmp = columnBitMask;
+          for (String qual : qualifiers) {
+            if ((columnMaskTmp & 1) != 0) {
+              scan.addColumn(FAMILY_BYTES, Bytes.toBytes(qual));
+              qualSet.add(qual);
+            }
+            columnMaskTmp >>= 1;
+          }
+          assertEquals(0, columnMaskTmp);
+        }
+
+        InternalScanner scanner = region.getScanner(scan);
+        List<KeyValue> results = new ArrayList<KeyValue>();
+
+        int kvPos = 0;
+        int numResults = 0;
+        String queryInfo = "columns queried: " + qualSet + " (columnBitMask="
+            + columnBitMask + "), maxVersions=" + maxVersions;
+
+        while (scanner.next(results) || results.size() > 0) {
+          for (KeyValue kv : results) {
+            while (kvPos < kvs.size()
+                && !matchesQuery(kvs.get(kvPos), qualSet, maxVersions,
+                    lastDelTimeMap)) {
+              ++kvPos;
+            }
+            String rowQual = getRowQualStr(kv);
+            String deleteInfo = "";
+            Long lastDelTS = lastDelTimeMap.get(rowQual);
+            if (lastDelTS != null) {
+              deleteInfo = "; last timestamp when row/column " + rowQual
+                  + " was deleted: " + lastDelTS;
+            }
+            assertTrue("Scanner returned additional key/value: " + kv + ", "
+                + queryInfo + deleteInfo + ";", kvPos < kvs.size());
+            assertEquals("Scanner returned wrong key/value; " + queryInfo
+                + deleteInfo + ";", kvs.get(kvPos), kv);
+            ++kvPos;
+            ++numResults;
+          }
+          results.clear();
+        }
+        for (; kvPos < kvs.size(); ++kvPos) {
+          KeyValue remainingKV = kvs.get(kvPos);
+          assertFalse("Matching column not returned by scanner: "
+              + remainingKV + ", " + queryInfo + ", results returned: "
+              + numResults, matchesQuery(remainingKV, qualSet, maxVersions,
+              lastDelTimeMap));
+        }
+      }
+    }
+    assertTrue("This test is supposed to delete at least some row/column "
+        + "pairs", lastDelTimeMap.size() > 0);
+    LOG.info("Number of row/col pairs deleted at least once: "
+        + lastDelTimeMap.size());
+  }
+
+  private static String getRowQualStr(KeyValue kv) {
+    String rowStr = Bytes.toString(kv.getBuffer(), kv.getRowOffset(),
+        kv.getRowLength());
+    String qualStr = Bytes.toString(kv.getBuffer(), kv.getQualifierOffset(),
+        kv.getQualifierLength());
+    return rowStr + "_" + qualStr;
+  }
+
+  private static boolean matchesQuery(KeyValue kv, Set<String> qualSet,
+      int maxVersions, Map<String, Long> lastDelTimeMap) {
+    Long lastDelTS = lastDelTimeMap.get(getRowQualStr(kv));
+    long ts = kv.getTimestamp();
+    return qualSet.contains(qualStr(kv))
+        && ts >= TIMESTAMPS[TIMESTAMPS.length - maxVersions]
+        && (lastDelTS == null || ts > lastDelTS);
+  }
+
+  private static String qualStr(KeyValue kv) {
+    return Bytes.toString(kv.getBuffer(), kv.getQualifierOffset(),
+        kv.getQualifierLength());
+  }
+
+  private static String createValue(String row, String qual, long ts) {
+    return "value_for_" + row + "_" + qual + "_" + ts;
+  }
+
+  private static List<String> sequentialStrings(String prefix, int n) {
+    List<String> lst = new ArrayList<String>();
+    for (int i = 0; i < n; ++i) {
+      StringBuilder sb = new StringBuilder();
+      sb.append(prefix + i);
+
+      // Make column length depend on i.
+      int iBitShifted = i;
+      while (iBitShifted != 0) {
+        sb.append((iBitShifted & 1) == 0 ? 'a' : 'b');
+        iBitShifted >>= 1;
+      }
+
+      lst.add(sb.toString());
+    }
+
+    return lst;
+  }
+
+}


Reply via email to