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.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023import static org.mockito.Mockito.mock;
024import static org.mockito.Mockito.when;
025
026import java.io.IOException;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.fs.FileSystem;
029import org.apache.hadoop.fs.Path;
030import org.apache.hadoop.hbase.HBaseTestingUtil;
031import org.apache.hadoop.hbase.KeyValue;
032import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
033import org.apache.hadoop.hbase.client.Get;
034import org.apache.hadoop.hbase.client.Scan;
035import org.apache.hadoop.hbase.io.hfile.CacheConfig;
036import org.apache.hadoop.hbase.io.hfile.HFileContext;
037import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder;
038import org.apache.hadoop.hbase.io.hfile.ReaderContext;
039import org.apache.hadoop.hbase.io.hfile.ReaderContextBuilder;
040import org.apache.hadoop.hbase.log.HBaseMarkers;
041import org.apache.hadoop.hbase.testclassification.RegionServerTests;
042import org.apache.hadoop.hbase.testclassification.SmallTests;
043import org.apache.hadoop.hbase.util.BloomFilterFactory;
044import org.apache.hadoop.hbase.util.BloomFilterUtil;
045import org.apache.hadoop.hbase.util.Bytes;
046import org.apache.hadoop.hbase.util.ChecksumType;
047import org.apache.hadoop.hbase.util.CommonFSUtils;
048import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
049import org.junit.jupiter.api.AfterEach;
050import org.junit.jupiter.api.BeforeEach;
051import org.junit.jupiter.api.Tag;
052import org.junit.jupiter.api.Test;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056/**
057 * Test TestRowPrefixBloomFilter
058 */
059@Tag(RegionServerTests.TAG)
060@Tag(SmallTests.TAG)
061public class TestRowPrefixBloomFilter {
062
063  private static final Logger LOG = LoggerFactory.getLogger(TestRowPrefixBloomFilter.class);
064  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
065  private CacheConfig cacheConf = new CacheConfig(TEST_UTIL.getConfiguration());
066  private static final ChecksumType CKTYPE = ChecksumType.CRC32C;
067  private static final int CKBYTES = 512;
068  private boolean localfs = false;
069  private static Configuration conf;
070  private static FileSystem fs;
071  private static Path testDir;
072  private static final int BLOCKSIZE_SMALL = 8192;
073  private static final float err = (float) 0.01;
074  private static final int prefixLength = 10;
075  private static final String invalidFormatter = "%08d";
076  private static final String prefixFormatter = "%010d";
077  private static final String suffixFormatter = "%010d";
078  private static final int prefixRowCount = 50;
079  private static final int suffixRowCount = 10;
080  private static final int fixedLengthExpKeys = prefixRowCount;
081  private static final BloomType bt = BloomType.ROWPREFIX_FIXED_LENGTH;
082
083  @BeforeEach
084  public void setUp() throws Exception {
085    conf = TEST_UTIL.getConfiguration();
086    conf.setFloat(BloomFilterFactory.IO_STOREFILE_BLOOM_ERROR_RATE, err);
087    conf.setBoolean(BloomFilterFactory.IO_STOREFILE_BLOOM_ENABLED, true);
088    conf.setInt(BloomFilterUtil.PREFIX_LENGTH_KEY, prefixLength);
089
090    localfs = (conf.get("fs.defaultFS", "file:///").compareTo("file:///") == 0);
091
092    if (fs == null) {
093      fs = FileSystem.get(conf);
094    }
095    try {
096      if (localfs) {
097        testDir = TEST_UTIL.getDataTestDir("TestRowPrefixBloomFilter");
098        if (fs.exists(testDir)) {
099          fs.delete(testDir, true);
100        }
101      } else {
102        testDir = CommonFSUtils.getRootDir(conf);
103      }
104    } catch (Exception e) {
105      LOG.error(HBaseMarkers.FATAL, "error during setup", e);
106      throw e;
107    }
108  }
109
110  @AfterEach
111  public void tearDown() throws Exception {
112    try {
113      if (localfs) {
114        if (fs.exists(testDir)) {
115          fs.delete(testDir, true);
116        }
117      }
118    } catch (Exception e) {
119      LOG.error(HBaseMarkers.FATAL, "error during tear down", e);
120    }
121  }
122
123  private static StoreFileScanner getStoreFileScanner(StoreFileReader reader) {
124    return reader.getStoreFileScanner(false, false, false, 0, 0, false);
125  }
126
127  private void writeStoreFile(final Path f, BloomType bt, int expKeys) throws IOException {
128    HFileContext meta = new HFileContextBuilder().withBlockSize(BLOCKSIZE_SMALL)
129      .withChecksumType(CKTYPE).withBytesPerCheckSum(CKBYTES).build();
130    // Make a store file and write data to it.
131    StoreFileWriter writer = new StoreFileWriter.Builder(conf, cacheConf, fs).withFilePath(f)
132      .withBloomType(bt).withMaxKeyCount(expKeys).withFileContext(meta).build();
133    long now = EnvironmentEdgeManager.currentTime();
134    try {
135      // Put with valid row style
136      for (int i = 0; i < prefixRowCount; i += 2) { // prefix rows
137        String prefixRow = String.format(prefixFormatter, i);
138        for (int j = 0; j < suffixRowCount; j++) { // suffix rows
139          String row = generateRowWithSuffix(prefixRow, j);
140          KeyValue kv = new KeyValue(Bytes.toBytes(row), Bytes.toBytes("family"),
141            Bytes.toBytes("col"), now, Bytes.toBytes("value"));
142          writer.append(kv);
143        }
144      }
145
146      // Put with invalid row style
147      for (int i = prefixRowCount; i < prefixRowCount * 2; i += 2) { // prefix rows
148        String row = String.format(invalidFormatter, i);
149        KeyValue kv = new KeyValue(Bytes.toBytes(row), Bytes.toBytes("family"),
150          Bytes.toBytes("col"), now, Bytes.toBytes("value"));
151        writer.append(kv);
152      }
153    } finally {
154      writer.close();
155    }
156  }
157
158  private String generateRowWithSuffix(String prefixRow, int suffix) {
159    StringBuilder row = new StringBuilder(prefixRow);
160    row.append("#");
161    row.append(String.format(suffixFormatter, suffix));
162    return row.toString();
163  }
164
165  @Test
166  public void testRowPrefixBloomFilter() throws Exception {
167    FileSystem fs = FileSystem.getLocal(conf);
168    float expErr = 2 * prefixRowCount * suffixRowCount * err;
169    int expKeys = fixedLengthExpKeys;
170    // write the file
171    if (!fs.exists(testDir)) {
172      fs.mkdirs(testDir);
173    }
174    Path f = StoreFileWriter.getUniqueFile(fs, testDir);
175    writeStoreFile(f, bt, expKeys);
176
177    // read the file
178    ReaderContext context = new ReaderContextBuilder().withFileSystemAndPath(fs, f).build();
179    StoreFileInfo storeFileInfo = StoreFileInfo.createStoreFileInfoForHFile(conf, fs, f, true);
180    storeFileInfo.initHFileInfo(context);
181    StoreFileReader reader = storeFileInfo.createReader(context, cacheConf);
182    storeFileInfo.getHFileInfo().initMetaAndIndex(reader.getHFileReader());
183    reader.loadFileInfo();
184    reader.loadBloomfilter();
185
186    // check basic param
187    assertEquals(bt, reader.getBloomFilterType());
188    assertEquals(prefixLength, reader.getPrefixLength());
189    assertEquals(expKeys, reader.getGeneralBloomFilter().getKeyCount());
190    StoreFileScanner scanner = getStoreFileScanner(reader);
191    HStore store = mock(HStore.class);
192    when(store.getColumnFamilyDescriptor()).thenReturn(ColumnFamilyDescriptorBuilder.of("family"));
193    // check false positives rate
194    int falsePos = 0;
195    int falseNeg = 0;
196    for (int i = 0; i < prefixRowCount; i++) { // prefix rows
197      String prefixRow = String.format(prefixFormatter, i);
198      for (int j = 0; j < suffixRowCount; j++) { // suffix rows
199        String startRow = generateRowWithSuffix(prefixRow, j);
200        String stopRow = generateRowWithSuffix(prefixRow, j + 1);
201        Scan scan =
202          new Scan().withStartRow(Bytes.toBytes(startRow)).withStopRow(Bytes.toBytes(stopRow));
203        boolean exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
204        boolean shouldPrefixRowExist = i % 2 == 0;
205        if (shouldPrefixRowExist) {
206          if (!exists) {
207            falseNeg++;
208          }
209        } else {
210          if (exists) {
211            falsePos++;
212          }
213        }
214      }
215    }
216
217    for (int i = prefixRowCount; i < prefixRowCount * 2; i++) { // prefix rows
218      String row = String.format(invalidFormatter, i);
219      Scan scan = new Scan(new Get(Bytes.toBytes(row)));
220      boolean exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
221      boolean shouldPrefixRowExist = i % 2 == 0;
222      if (shouldPrefixRowExist) {
223        if (!exists) {
224          falseNeg++;
225        }
226      } else {
227        if (exists) {
228          falsePos++;
229        }
230      }
231    }
232    reader.close(true); // evict because we are about to delete the file
233    fs.delete(f, true);
234    assertEquals(0, falseNeg, "False negatives: " + falseNeg);
235    int maxFalsePos = (int) (2 * expErr);
236    assertTrue(falsePos <= maxFalsePos, "Too many false positives: " + falsePos + " (err=" + err
237      + ", expected no more than " + maxFalsePos + ")");
238  }
239
240  @Test
241  public void testRowPrefixBloomFilterWithGet() throws Exception {
242    FileSystem fs = FileSystem.getLocal(conf);
243    int expKeys = fixedLengthExpKeys;
244    // write the file
245    if (!fs.exists(testDir)) {
246      fs.mkdirs(testDir);
247    }
248    Path f = StoreFileWriter.getUniqueFile(fs, testDir);
249    writeStoreFile(f, bt, expKeys);
250
251    ReaderContext context = new ReaderContextBuilder().withFileSystemAndPath(fs, f).build();
252    StoreFileInfo storeFileInfo = StoreFileInfo.createStoreFileInfoForHFile(conf, fs, f, true);
253    storeFileInfo.initHFileInfo(context);
254    StoreFileReader reader = storeFileInfo.createReader(context, cacheConf);
255    storeFileInfo.getHFileInfo().initMetaAndIndex(reader.getHFileReader());
256    reader.loadFileInfo();
257    reader.loadBloomfilter();
258
259    StoreFileScanner scanner = getStoreFileScanner(reader);
260    HStore store = mock(HStore.class);
261    when(store.getColumnFamilyDescriptor()).thenReturn(ColumnFamilyDescriptorBuilder.of("family"));
262
263    // Get with valid row style
264    // prefix row in bloom
265    String prefixRow = String.format(prefixFormatter, prefixRowCount - 2);
266    String row = generateRowWithSuffix(prefixRow, 0);
267    Scan scan = new Scan(new Get(Bytes.toBytes(row)));
268    boolean exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
269    assertTrue(exists);
270
271    // prefix row not in bloom
272    prefixRow = String.format(prefixFormatter, prefixRowCount - 1);
273    row = generateRowWithSuffix(prefixRow, 0);
274    scan = new Scan(new Get(Bytes.toBytes(row)));
275    exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
276    assertFalse(exists);
277
278    // Get with invalid row style
279    // ROWPREFIX: the length of row is less than prefixLength
280    // row in bloom
281    row = String.format(invalidFormatter, prefixRowCount + 2);
282    scan = new Scan(new Get(Bytes.toBytes(row)));
283    exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
284    assertTrue(exists);
285
286    // row not in bloom
287    row = String.format(invalidFormatter, prefixRowCount + 1);
288    scan = new Scan(new Get(Bytes.toBytes(row)));
289    exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
290    assertFalse(exists);
291
292    reader.close(true); // evict because we are about to delete the file
293    fs.delete(f, true);
294  }
295
296  @Test
297  public void testRowPrefixBloomFilterWithScan() throws Exception {
298    FileSystem fs = FileSystem.getLocal(conf);
299    int expKeys = fixedLengthExpKeys;
300    // write the file
301    if (!fs.exists(testDir)) {
302      fs.mkdirs(testDir);
303    }
304    Path f = StoreFileWriter.getUniqueFile(fs, testDir);
305    writeStoreFile(f, bt, expKeys);
306
307    ReaderContext context = new ReaderContextBuilder().withFileSystemAndPath(fs, f).build();
308    StoreFileInfo storeFileInfo = StoreFileInfo.createStoreFileInfoForHFile(conf, fs, f, true);
309    storeFileInfo.initHFileInfo(context);
310    StoreFileReader reader = storeFileInfo.createReader(context, cacheConf);
311    storeFileInfo.getHFileInfo().initMetaAndIndex(reader.getHFileReader());
312    reader.loadFileInfo();
313    reader.loadBloomfilter();
314
315    StoreFileScanner scanner = getStoreFileScanner(reader);
316    HStore store = mock(HStore.class);
317    when(store.getColumnFamilyDescriptor()).thenReturn(ColumnFamilyDescriptorBuilder.of("family"));
318
319    // Scan with valid row style. startRow and stopRow have a common prefix.
320    // And the length of the common prefix is no less than prefixLength.
321    // prefix row in bloom
322    String prefixRow = String.format(prefixFormatter, prefixRowCount - 2);
323    String startRow = generateRowWithSuffix(prefixRow, 0);
324    String stopRow = generateRowWithSuffix(prefixRow, 1);
325    Scan scan =
326      new Scan().withStartRow(Bytes.toBytes(startRow)).withStopRow(Bytes.toBytes(stopRow));
327    boolean exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
328    assertTrue(exists);
329
330    // prefix row not in bloom
331    prefixRow = String.format(prefixFormatter, prefixRowCount - 1);
332    startRow = generateRowWithSuffix(prefixRow, 0);
333    stopRow = generateRowWithSuffix(prefixRow, 1);
334    scan = new Scan().withStartRow(Bytes.toBytes(startRow)).withStopRow(Bytes.toBytes(stopRow));
335    exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
336    assertFalse(exists);
337
338    // There is no common prefix between startRow and stopRow.
339    prefixRow = String.format(prefixFormatter, prefixRowCount - 2);
340    startRow = generateRowWithSuffix(prefixRow, 0);
341    scan = new Scan().withStartRow(Bytes.toBytes(startRow));
342    exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
343    assertTrue(exists);
344
345    // startRow and stopRow have a common prefix.
346    // But the length of the common prefix is less than prefixLength.
347    String prefixStartRow = String.format(prefixFormatter, prefixRowCount - 2);
348    String prefixStopRow = String.format(prefixFormatter, prefixRowCount - 1);
349    startRow = generateRowWithSuffix(prefixStartRow, 0);
350    stopRow = generateRowWithSuffix(prefixStopRow, 0);
351    scan = new Scan().withStartRow(Bytes.toBytes(startRow)).withStopRow(Bytes.toBytes(stopRow));
352    exists = scanner.shouldUseScanner(scan, store, Long.MIN_VALUE);
353    assertTrue(exists);
354
355    reader.close(true); // evict because we are about to delete the file
356    fs.delete(f, true);
357  }
358}