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.apache.hadoop.hbase.HBaseTestingUtil.START_KEY;
021import static org.apache.hadoop.hbase.HBaseTestingUtil.START_KEY_BYTES;
022import static org.apache.hadoop.hbase.HBaseTestingUtil.fam1;
023import static org.apache.hadoop.hbase.regionserver.Store.PRIORITY_USER;
024import static org.junit.jupiter.api.Assertions.assertEquals;
025import static org.junit.jupiter.api.Assertions.assertNotNull;
026import static org.junit.jupiter.api.Assertions.assertTrue;
027
028import java.io.IOException;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.Map.Entry;
035import java.util.stream.Stream;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.hbase.Cell;
038import org.apache.hadoop.hbase.CellUtil;
039import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate;
040import org.apache.hadoop.hbase.HBaseTestingUtil;
041import org.apache.hadoop.hbase.HConstants;
042import org.apache.hadoop.hbase.HTestConst;
043import org.apache.hadoop.hbase.KeepDeletedCells;
044import org.apache.hadoop.hbase.KeyValue;
045import org.apache.hadoop.hbase.TableName;
046import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
047import org.apache.hadoop.hbase.client.Delete;
048import org.apache.hadoop.hbase.client.Get;
049import org.apache.hadoop.hbase.client.Result;
050import org.apache.hadoop.hbase.client.Scan;
051import org.apache.hadoop.hbase.client.Table;
052import org.apache.hadoop.hbase.client.TableDescriptor;
053import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
054import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoder;
055import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoderImpl;
056import org.apache.hadoop.hbase.regionserver.compactions.CompactionLifeCycleTracker;
057import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequestImpl;
058import org.apache.hadoop.hbase.regionserver.compactions.RatioBasedCompactionPolicy;
059import org.apache.hadoop.hbase.testclassification.LargeTests;
060import org.apache.hadoop.hbase.testclassification.RegionServerTests;
061import org.apache.hadoop.hbase.util.Bytes;
062import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
063import org.apache.hadoop.hbase.wal.WAL;
064import org.junit.jupiter.api.AfterEach;
065import org.junit.jupiter.api.BeforeEach;
066import org.junit.jupiter.api.Tag;
067import org.junit.jupiter.api.TestInfo;
068import org.junit.jupiter.api.TestTemplate;
069import org.junit.jupiter.params.provider.Arguments;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072
073/**
074 * Test major compactions
075 */
076@Tag(RegionServerTests.TAG)
077@Tag(LargeTests.TAG)
078@HBaseParameterizedTestTemplate(name = "{index}: compType={0}")
079public class TestMajorCompaction {
080
081  public static Stream<Arguments> parameters() {
082    return Stream.of("NONE", "BASIC", "EAGER").map(Arguments::of);
083  }
084
085  private static final Logger LOG = LoggerFactory.getLogger(TestMajorCompaction.class.getName());
086  private static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
087  protected Configuration conf = UTIL.getConfiguration();
088
089  private String name;
090  private final String compType;
091
092  private HRegion r = null;
093  private TableDescriptor htd = null;
094  private static final byte[] COLUMN_FAMILY = fam1;
095  private final byte[] STARTROW = Bytes.toBytes(START_KEY);
096  private static final byte[] COLUMN_FAMILY_TEXT = COLUMN_FAMILY;
097  private int compactionThreshold;
098  private byte[] secondRowBytes, thirdRowBytes;
099  private static final long MAX_FILES_TO_COMPACT = 10;
100
101  /** constructor */
102  public TestMajorCompaction(String compType) {
103    super();
104    this.compType = compType;
105    // Set cache flush size to 1MB
106    conf.setInt(HConstants.HREGION_MEMSTORE_FLUSH_SIZE, 1024 * 1024);
107    conf.setInt(HConstants.HREGION_MEMSTORE_BLOCK_MULTIPLIER, 100);
108    compactionThreshold = conf.getInt("hbase.hstore.compactionThreshold", 3);
109    conf.set(CompactingMemStore.COMPACTING_MEMSTORE_TYPE_KEY, String.valueOf(compType));
110
111    secondRowBytes = START_KEY_BYTES.clone();
112    // Increment the least significant character so we get to next row.
113    secondRowBytes[START_KEY_BYTES.length - 1]++;
114    thirdRowBytes = START_KEY_BYTES.clone();
115    thirdRowBytes[START_KEY_BYTES.length - 1] =
116      (byte) (thirdRowBytes[START_KEY_BYTES.length - 1] + 2);
117  }
118
119  @BeforeEach
120  public void setUp(TestInfo testInfo) throws Exception {
121    this.name = testInfo.getTestMethod().get().getName();
122    this.htd = UTIL.createTableDescriptor(
123      TableName.valueOf((name + "-" + compType).replace('[', 'i').replace(']', 'i')),
124      ColumnFamilyDescriptorBuilder.DEFAULT_MIN_VERSIONS, 3, HConstants.FOREVER,
125      ColumnFamilyDescriptorBuilder.DEFAULT_KEEP_DELETED);
126    this.r = UTIL.createLocalHRegion(htd, null, null);
127  }
128
129  @AfterEach
130  public void tearDown() throws Exception {
131    WAL wal = ((HRegion) r).getWAL();
132    ((HRegion) r).close();
133    wal.close();
134  }
135
136  /**
137   * Test that on a major compaction, if all cells are expired or deleted, then we'll end up with no
138   * product. Make sure scanner over region returns right answer in this case - and that it just
139   * basically works.
140   * @throws IOException exception encountered
141   */
142  @TestTemplate
143  public void testMajorCompactingToNoOutput() throws IOException {
144    testMajorCompactingWithDeletes(KeepDeletedCells.FALSE);
145  }
146
147  /**
148   * Test that on a major compaction,Deleted cells are retained if keep deleted cells is set to true
149   * @throws IOException exception encountered
150   */
151  @TestTemplate
152  public void testMajorCompactingWithKeepDeletedCells() throws IOException {
153    testMajorCompactingWithDeletes(KeepDeletedCells.TRUE);
154  }
155
156  /**
157   * Run compaction and flushing memstore Assert deletes get cleaned up.
158   */
159  @TestTemplate
160  public void testMajorCompaction() throws Exception {
161    majorCompaction();
162  }
163
164  @TestTemplate
165  public void testDataBlockEncodingInCacheOnly() throws Exception {
166    majorCompactionWithDataBlockEncoding(true);
167  }
168
169  @TestTemplate
170  public void testDataBlockEncodingEverywhere() throws Exception {
171    majorCompactionWithDataBlockEncoding(false);
172  }
173
174  public void majorCompactionWithDataBlockEncoding(boolean inCacheOnly) throws Exception {
175    Map<HStore, HFileDataBlockEncoder> replaceBlockCache = new HashMap<>();
176    for (HStore store : r.getStores()) {
177      HFileDataBlockEncoder blockEncoder = store.getDataBlockEncoder();
178      replaceBlockCache.put(store, blockEncoder);
179      final DataBlockEncoding inCache = DataBlockEncoding.PREFIX;
180      final DataBlockEncoding onDisk = inCacheOnly ? DataBlockEncoding.NONE : inCache;
181      ((HStore) store).setDataBlockEncoderInTest(new HFileDataBlockEncoderImpl(onDisk));
182    }
183
184    majorCompaction();
185
186    // restore settings
187    for (Entry<HStore, HFileDataBlockEncoder> entry : replaceBlockCache.entrySet()) {
188      ((HStore) entry.getKey()).setDataBlockEncoderInTest(entry.getValue());
189    }
190  }
191
192  private void majorCompaction() throws Exception {
193    createStoreFile(r);
194    for (int i = 0; i < compactionThreshold; i++) {
195      createStoreFile(r);
196    }
197    // Add more content.
198    HTestConst.addContent(new RegionAsTable(r), Bytes.toString(COLUMN_FAMILY));
199
200    // Now there are about 5 versions of each column.
201    // Default is that there only 3 (MAXVERSIONS) versions allowed per column.
202    //
203    // Assert == 3 when we ask for versions.
204    Result result = r.get(new Get(STARTROW).addFamily(COLUMN_FAMILY_TEXT).readVersions(100));
205    assertEquals(compactionThreshold, result.size());
206
207    r.flush(true);
208    r.compact(true);
209
210    // look at the second row
211    // Increment the least significant character so we get to next row.
212    byte[] secondRowBytes = START_KEY_BYTES.clone();
213    secondRowBytes[START_KEY_BYTES.length - 1]++;
214
215    // Always 3 versions if that is what max versions is.
216    result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100));
217    LOG.debug(
218      "Row " + Bytes.toStringBinary(secondRowBytes) + " after " + "initial compaction: " + result);
219    assertEquals(compactionThreshold, result.size(),
220      "Invalid number of versions of row " + Bytes.toStringBinary(secondRowBytes) + ".");
221
222    // Now add deletes to memstore and then flush it.
223    // That will put us over
224    // the compaction threshold of 3 store files. Compacting these store files
225    // should result in a compacted store file that has no references to the
226    // deleted row.
227    LOG.debug("Adding deletes to memstore and flushing");
228    Delete delete = new Delete(secondRowBytes, EnvironmentEdgeManager.currentTime());
229    byte[][] famAndQf = { COLUMN_FAMILY, null };
230    delete.addFamily(famAndQf[0]);
231    r.delete(delete);
232
233    // Assert deleted.
234    result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100));
235    assertTrue(result.isEmpty(), "Second row should have been deleted");
236
237    r.flush(true);
238
239    result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100));
240    assertTrue(result.isEmpty(), "Second row should have been deleted");
241
242    // Add a bit of data and flush. Start adding at 'bbb'.
243    createSmallerStoreFile(this.r);
244    r.flush(true);
245    // Assert that the second row is still deleted.
246    result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100));
247    assertTrue(result.isEmpty(), "Second row should still be deleted");
248
249    // Force major compaction.
250    r.compact(true);
251    assertEquals(1, r.getStore(COLUMN_FAMILY_TEXT).getStorefiles().size());
252
253    result = r.get(new Get(secondRowBytes).addFamily(COLUMN_FAMILY_TEXT).readVersions(100));
254    assertTrue(result.isEmpty(), "Second row should still be deleted");
255
256    // Make sure the store files do have some 'aaa' keys in them -- exactly 3.
257    // Also, that compacted store files do not have any secondRowBytes because
258    // they were deleted.
259    verifyCounts(3, 0);
260
261    // Multiple versions allowed for an entry, so the delete isn't enough
262    // Lower TTL and expire to ensure that all our entries have been wiped
263    final int ttl = 1000;
264    for (HStore store : r.getStores()) {
265      ScanInfo old = store.getScanInfo();
266      ScanInfo si = old.customize(old.getMaxVersions(), ttl, old.getKeepDeletedCells());
267      store.setScanInfo(si);
268    }
269    Thread.sleep(1000);
270
271    r.compact(true);
272    int count = count();
273    assertEquals(0, count, "Should not see anything after TTL has expired");
274  }
275
276  @TestTemplate
277  public void testTimeBasedMajorCompaction() throws Exception {
278    // create 2 storefiles and force a major compaction to reset the time
279    int delay = 10 * 1000; // 10 sec
280    float jitterPct = 0.20f; // 20%
281    conf.setLong(HConstants.MAJOR_COMPACTION_PERIOD, delay);
282    conf.setFloat("hbase.hregion.majorcompaction.jitter", jitterPct);
283
284    HStore s = ((HStore) r.getStore(COLUMN_FAMILY));
285    s.storeEngine.getCompactionPolicy().setConf(conf);
286    try {
287      createStoreFile(r);
288      createStoreFile(r);
289      r.compact(true);
290
291      // add one more file & verify that a regular compaction won't work
292      createStoreFile(r);
293      r.compact(false);
294      assertEquals(2, s.getStorefilesCount());
295
296      // ensure that major compaction time is deterministic
297      RatioBasedCompactionPolicy c =
298        (RatioBasedCompactionPolicy) s.storeEngine.getCompactionPolicy();
299      Collection<HStoreFile> storeFiles = s.getStorefiles();
300      long mcTime = c.getNextMajorCompactTime(storeFiles);
301      for (int i = 0; i < 10; ++i) {
302        assertEquals(mcTime, c.getNextMajorCompactTime(storeFiles));
303      }
304
305      // ensure that the major compaction time is within the variance
306      long jitter = Math.round(delay * jitterPct);
307      assertTrue(delay - jitter <= mcTime && mcTime <= delay + jitter);
308
309      // wait until the time-based compaction interval
310      Thread.sleep(mcTime);
311
312      // trigger a compaction request and ensure that it's upgraded to major
313      r.compact(false);
314      assertEquals(1, s.getStorefilesCount());
315    } finally {
316      // reset the timed compaction settings
317      conf.setLong(HConstants.MAJOR_COMPACTION_PERIOD, 1000 * 60 * 60 * 24);
318      conf.setFloat("hbase.hregion.majorcompaction.jitter", 0.20F);
319      // run a major to reset the cache
320      createStoreFile(r);
321      r.compact(true);
322      assertEquals(1, s.getStorefilesCount());
323    }
324  }
325
326  private void verifyCounts(int countRow1, int countRow2) throws Exception {
327    int count1 = 0;
328    int count2 = 0;
329    for (HStoreFile f : r.getStore(COLUMN_FAMILY_TEXT).getStorefiles()) {
330      try (StoreFileScanner scanner = f.getPreadScanner(false, Long.MAX_VALUE, 0, false)) {
331        scanner.seek(KeyValue.LOWESTKEY);
332        for (Cell cell;;) {
333          cell = scanner.next();
334          if (cell == null) {
335            break;
336          }
337          byte[] row = CellUtil.cloneRow(cell);
338          if (Bytes.equals(row, STARTROW)) {
339            count1++;
340          } else if (Bytes.equals(row, secondRowBytes)) {
341            count2++;
342          }
343        }
344      }
345    }
346    assertEquals(countRow1, count1);
347    assertEquals(countRow2, count2);
348  }
349
350  private int count() throws IOException {
351    int count = 0;
352    for (HStoreFile f : r.getStore(COLUMN_FAMILY_TEXT).getStorefiles()) {
353      try (StoreFileScanner scanner = f.getPreadScanner(false, Long.MAX_VALUE, 0, false)) {
354        scanner.seek(KeyValue.LOWESTKEY);
355        while (scanner.next() != null) {
356          count++;
357        }
358      }
359    }
360    return count;
361  }
362
363  private void createStoreFile(final HRegion region) throws IOException {
364    createStoreFile(region, Bytes.toString(COLUMN_FAMILY));
365  }
366
367  private void createStoreFile(final HRegion region, String family) throws IOException {
368    Table loader = new RegionAsTable(region);
369    HTestConst.addContent(loader, family);
370    region.flush(true);
371  }
372
373  private void createSmallerStoreFile(final HRegion region) throws IOException {
374    Table loader = new RegionAsTable(region);
375    HTestConst.addContent(loader, Bytes.toString(COLUMN_FAMILY), Bytes.toBytes("" + "bbb"), null);
376    region.flush(true);
377  }
378
379  /**
380   * Test for HBASE-5920 - Test user requested major compactions always occurring
381   */
382  @TestTemplate
383  public void testNonUserMajorCompactionRequest() throws Exception {
384    HStore store = r.getStore(COLUMN_FAMILY);
385    createStoreFile(r);
386    for (int i = 0; i < MAX_FILES_TO_COMPACT + 1; i++) {
387      createStoreFile(r);
388    }
389    store.triggerMajorCompaction();
390
391    CompactionRequestImpl request = store.requestCompaction().get().getRequest();
392    assertNotNull(request, "Expected to receive a compaction request");
393    assertEquals(false, request.isMajor(),
394      "System-requested major compaction should not occur if there are too many store files");
395  }
396
397  /**
398   * Test for HBASE-5920
399   */
400  @TestTemplate
401  public void testUserMajorCompactionRequest() throws IOException {
402    HStore store = r.getStore(COLUMN_FAMILY);
403    createStoreFile(r);
404    for (int i = 0; i < MAX_FILES_TO_COMPACT + 1; i++) {
405      createStoreFile(r);
406    }
407    store.triggerMajorCompaction();
408    CompactionRequestImpl request = store
409      .requestCompaction(PRIORITY_USER, CompactionLifeCycleTracker.DUMMY, null).get().getRequest();
410    assertNotNull(request, "Expected to receive a compaction request");
411    assertTrue(request.isMajor(), "User-requested major compaction should always occur, "
412      + "even if there are too many store files");
413  }
414
415  /**
416   * Test that on a major compaction, if all cells are expired or deleted, then we'll end up with no
417   * product. Make sure scanner over region returns right answer in this case - and that it just
418   * basically works.
419   */
420  @TestTemplate
421  public void testMajorCompactingToNoOutputWithReverseScan() throws IOException {
422    createStoreFile(r);
423    for (int i = 0; i < compactionThreshold; i++) {
424      createStoreFile(r);
425    }
426    // Now delete everything.
427    Scan scan = new Scan();
428    scan.setReversed(true);
429    InternalScanner s = r.getScanner(scan);
430    do {
431      List<Cell> results = new ArrayList<>();
432      boolean result = s.next(results);
433      assertTrue(!results.isEmpty());
434      r.delete(new Delete(CellUtil.cloneRow(results.get(0))));
435      if (!result) {
436        break;
437      }
438    } while (true);
439    s.close();
440    // Flush
441    r.flush(true);
442    // Major compact.
443    r.compact(true);
444    scan = new Scan();
445    scan.setReversed(true);
446    s = r.getScanner(scan);
447    int counter = 0;
448    do {
449      List<Cell> results = new ArrayList<>();
450      boolean result = s.next(results);
451      if (!result) {
452        break;
453      }
454      counter++;
455    } while (true);
456    s.close();
457    assertEquals(0, counter);
458  }
459
460  private void testMajorCompactingWithDeletes(KeepDeletedCells keepDeletedCells)
461    throws IOException {
462    createStoreFile(r);
463    for (int i = 0; i < compactionThreshold; i++) {
464      createStoreFile(r);
465    }
466    // Now delete everything.
467    InternalScanner s = r.getScanner(new Scan());
468    int originalCount = 0;
469    do {
470      List<Cell> results = new ArrayList<>();
471      boolean result = s.next(results);
472      r.delete(new Delete(CellUtil.cloneRow(results.get(0))));
473      if (!result) break;
474      originalCount++;
475    } while (true);
476    s.close();
477    // Flush
478    r.flush(true);
479
480    for (HStore store : this.r.stores.values()) {
481      ScanInfo old = store.getScanInfo();
482      ScanInfo si = old.customize(old.getMaxVersions(), old.getTtl(), keepDeletedCells);
483      store.setScanInfo(si);
484    }
485    // Major compact.
486    r.compact(true);
487    s = r.getScanner(new Scan().setRaw(true));
488    int counter = 0;
489    do {
490      List<Cell> results = new ArrayList<>();
491      boolean result = s.next(results);
492      if (!result) break;
493      counter++;
494    } while (true);
495    assertEquals(keepDeletedCells == KeepDeletedCells.TRUE ? originalCount : 0, counter);
496
497  }
498}