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