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.HBaseTestingUtility.START_KEY;
021import static org.apache.hadoop.hbase.HBaseTestingUtility.START_KEY_BYTES;
022import static org.apache.hadoop.hbase.HBaseTestingUtility.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.HBaseTestingUtility;
040import org.apache.hadoop.hbase.HConstants;
041import org.apache.hadoop.hbase.HTableDescriptor;
042import org.apache.hadoop.hbase.HTestConst;
043import org.apache.hadoop.hbase.KeepDeletedCells;
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.io.encoding.DataBlockEncoding;
052import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoder;
053import org.apache.hadoop.hbase.io.hfile.HFileDataBlockEncoderImpl;
054import org.apache.hadoop.hbase.io.hfile.HFileScanner;
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 HBaseTestingUtility UTIL = HBaseTestingUtility.createLocalHTU();
095  protected Configuration conf = UTIL.getConfiguration();
096
097  private HRegion r = null;
098  private HTableDescriptor 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      HFileScanner scanner = f.getReader().getScanner(false, false);
335      scanner.seekTo();
336      do {
337        byte[] row = CellUtil.cloneRow(scanner.getCell());
338        if (Bytes.equals(row, STARTROW)) {
339          count1++;
340        } else if (Bytes.equals(row, secondRowBytes)) {
341          count2++;
342        }
343      } while (scanner.next());
344    }
345    assertEquals(countRow1, count1);
346    assertEquals(countRow2, count2);
347  }
348
349  private int count() throws IOException {
350    int count = 0;
351    for (HStoreFile f : r.getStore(COLUMN_FAMILY_TEXT).getStorefiles()) {
352      HFileScanner scanner = f.getReader().getScanner(false, false);
353      if (!scanner.seekTo()) {
354        continue;
355      }
356      do {
357        count++;
358      } while (scanner.next());
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  @Test
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("Expected to receive a compaction request", request);
393    assertEquals(
394      "System-requested major compaction should not occur if there are too many store files", false,
395      request.isMajor());
396  }
397
398  /**
399   * Test for HBASE-5920
400   */
401  @Test
402  public void testUserMajorCompactionRequest() throws IOException {
403    HStore store = r.getStore(COLUMN_FAMILY);
404    createStoreFile(r);
405    for (int i = 0; i < MAX_FILES_TO_COMPACT + 1; i++) {
406      createStoreFile(r);
407    }
408    store.triggerMajorCompaction();
409    CompactionRequestImpl request = store
410      .requestCompaction(PRIORITY_USER, CompactionLifeCycleTracker.DUMMY, null).get().getRequest();
411    assertNotNull("Expected to receive a compaction request", request);
412    assertEquals(
413      "User-requested major compaction should always occur, even if there are too many store files",
414      true, request.isMajor());
415  }
416
417  /**
418   * Test that on a major compaction, if all cells are expired or deleted, then we'll end up with no
419   * product. Make sure scanner over region returns right answer in this case - and that it just
420   * basically works.
421   */
422  @Test
423  public void testMajorCompactingToNoOutputWithReverseScan() throws IOException {
424    createStoreFile(r);
425    for (int i = 0; i < compactionThreshold; i++) {
426      createStoreFile(r);
427    }
428    // Now delete everything.
429    Scan scan = new Scan();
430    scan.setReversed(true);
431    InternalScanner s = r.getScanner(scan);
432    do {
433      List<Cell> results = new ArrayList<>();
434      boolean result = s.next(results);
435      assertTrue(!results.isEmpty());
436      r.delete(new Delete(CellUtil.cloneRow(results.get(0))));
437      if (!result) {
438        break;
439      }
440    } while (true);
441    s.close();
442    // Flush
443    r.flush(true);
444    // Major compact.
445    r.compact(true);
446    scan = new Scan();
447    scan.setReversed(true);
448    s = r.getScanner(scan);
449    int counter = 0;
450    do {
451      List<Cell> results = new ArrayList<>();
452      boolean result = s.next(results);
453      if (!result) {
454        break;
455      }
456      counter++;
457    } while (true);
458    s.close();
459    assertEquals(0, counter);
460  }
461
462  private void testMajorCompactingWithDeletes(KeepDeletedCells keepDeletedCells)
463    throws IOException {
464    createStoreFile(r);
465    for (int i = 0; i < compactionThreshold; i++) {
466      createStoreFile(r);
467    }
468    // Now delete everything.
469    InternalScanner s = r.getScanner(new Scan());
470    int originalCount = 0;
471    do {
472      List<Cell> results = new ArrayList<>();
473      boolean result = s.next(results);
474      r.delete(new Delete(CellUtil.cloneRow(results.get(0))));
475      if (!result) break;
476      originalCount++;
477    } while (true);
478    s.close();
479    // Flush
480    r.flush(true);
481
482    for (HStore store : this.r.stores.values()) {
483      ScanInfo old = store.getScanInfo();
484      ScanInfo si = old.customize(old.getMaxVersions(), old.getTtl(), keepDeletedCells);
485      store.setScanInfo(si);
486    }
487    // Major compact.
488    r.compact(true);
489    s = r.getScanner(new Scan().setRaw(true));
490    int counter = 0;
491    do {
492      List<Cell> results = new ArrayList<>();
493      boolean result = s.next(results);
494      if (!result) break;
495      counter++;
496    } while (true);
497    assertEquals(keepDeletedCells == KeepDeletedCells.TRUE ? originalCount : 0, counter);
498
499  }
500}