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