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