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.snapshot;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertNotNull;
022import static org.junit.Assert.assertTrue;
023import static org.mockito.Mockito.doAnswer;
024import static org.mockito.Mockito.spy;
025
026import java.io.IOException;
027import java.util.List;
028import java.util.concurrent.ExecutorService;
029import java.util.concurrent.Executors;
030import java.util.concurrent.Future;
031import org.apache.hadoop.conf.Configuration;
032import org.apache.hadoop.fs.FileSystem;
033import org.apache.hadoop.fs.Path;
034import org.apache.hadoop.hbase.HBaseClassTestRule;
035import org.apache.hadoop.hbase.HBaseTestingUtility;
036import org.apache.hadoop.hbase.TableName;
037import org.apache.hadoop.hbase.client.Table;
038import org.apache.hadoop.hbase.client.TableDescriptor;
039import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
040import org.apache.hadoop.hbase.errorhandling.ForeignExceptionDispatcher;
041import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy;
042import org.apache.hadoop.hbase.regionserver.HRegion;
043import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
044import org.apache.hadoop.hbase.regionserver.snapshot.FlushSnapshotSubprocedure;
045import org.apache.hadoop.hbase.testclassification.MediumTests;
046import org.apache.hadoop.hbase.testclassification.RegionServerTests;
047import org.apache.hadoop.hbase.util.Bytes;
048import org.apache.hadoop.hbase.util.CommonFSUtils;
049import org.junit.AfterClass;
050import org.junit.BeforeClass;
051import org.junit.ClassRule;
052import org.junit.Test;
053import org.junit.experimental.categories.Category;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos;
058
059/**
060 * Testing the region snapshot task on a cluster.
061 * @see org.apache.hadoop.hbase.regionserver.snapshot.FlushSnapshotSubprocedure.RegionSnapshotTask
062 */
063@Category({ MediumTests.class, RegionServerTests.class })
064public class TestRegionSnapshotTask {
065
066  @ClassRule
067  public static final HBaseClassTestRule CLASS_RULE =
068    HBaseClassTestRule.forClass(TestRegionSnapshotTask.class);
069
070  private final Logger LOG = LoggerFactory.getLogger(getClass());
071
072  private static HBaseTestingUtility TEST_UTIL;
073  private static Configuration conf;
074  private static FileSystem fs;
075  private static Path rootDir;
076
077  @BeforeClass
078  public static void setupBeforeClass() throws Exception {
079    TEST_UTIL = new HBaseTestingUtility();
080
081    conf = TEST_UTIL.getConfiguration();
082
083    // Try to frequently clean up compacted files
084    conf.setInt("hbase.hfile.compaction.discharger.interval", 1000);
085    conf.setInt("hbase.master.hfilecleaner.ttl", 1000);
086
087    TEST_UTIL.startMiniCluster(1);
088    TEST_UTIL.getHBaseCluster().waitForActiveAndReadyMaster();
089    TEST_UTIL.waitUntilAllRegionsAssigned(TableName.META_TABLE_NAME);
090
091    rootDir = CommonFSUtils.getRootDir(conf);
092    fs = TEST_UTIL.getTestFileSystem();
093  }
094
095  @AfterClass
096  public static void tearDown() throws Exception {
097    TEST_UTIL.shutdownMiniCluster();
098  }
099
100  /**
101   * Tests adding a region to the snapshot manifest while compactions are running on the region. The
102   * idea is to slow down the process of adding a store file to the manifest while triggering
103   * compactions on the region, allowing the store files to be marked for archival while snapshot
104   * operation is running. This test checks for the correct behavior in such a case that the
105   * compacted files should not be moved around if a snapshot operation is in progress. See
106   * HBASE-18398
107   */
108  @Test
109  public void testAddRegionWithCompactions() throws Exception {
110    final TableName tableName = TableName.valueOf("test_table");
111    Table table = setupTable(tableName);
112
113    List<HRegion> hRegions = TEST_UTIL.getHBaseCluster().getRegions(tableName);
114
115    final SnapshotProtos.SnapshotDescription snapshot =
116      SnapshotProtos.SnapshotDescription.newBuilder().setTable(tableName.getNameAsString())
117        .setType(SnapshotProtos.SnapshotDescription.Type.FLUSH).setName("test_table_snapshot")
118        .setVersion(SnapshotManifestV2.DESCRIPTOR_VERSION).build();
119    ForeignExceptionDispatcher monitor = new ForeignExceptionDispatcher(snapshot.getName());
120
121    final HRegion region = spy(hRegions.get(0));
122
123    Path workingDir = SnapshotDescriptionUtils.getWorkingSnapshotDir(snapshot, rootDir, conf);
124    final SnapshotManifest manifest =
125      SnapshotManifest.create(conf, fs, workingDir, snapshot, monitor);
126    manifest.addTableDescriptor(table.getTableDescriptor());
127
128    if (!fs.exists(workingDir)) {
129      fs.mkdirs(workingDir);
130    }
131    assertTrue(fs.exists(workingDir));
132    SnapshotDescriptionUtils.writeSnapshotInfo(snapshot, workingDir, fs);
133
134    doAnswer(__ -> {
135      addRegionToSnapshot(snapshot, region, manifest);
136      return null;
137    }).when(region).addRegionToSnapshot(snapshot, monitor);
138
139    FlushSnapshotSubprocedure.RegionSnapshotTask snapshotTask =
140      new FlushSnapshotSubprocedure.RegionSnapshotTask(region, snapshot, true, monitor);
141    ExecutorService executor = Executors.newFixedThreadPool(1);
142    Future f = executor.submit(snapshotTask);
143
144    // Trigger major compaction and wait for snaphot operation to finish
145    LOG.info("Starting major compaction");
146    region.compact(true);
147    LOG.info("Finished major compaction");
148    f.get();
149
150    // Consolidate region manifests into a single snapshot manifest
151    manifest.consolidate();
152
153    // Make sure that the region manifest exists, which means the snapshot operation succeeded
154    assertNotNull(manifest.getRegionManifests());
155    // Sanity check, there should be only one region
156    assertEquals(1, manifest.getRegionManifests().size());
157
158    // Make sure that no files went missing after the snapshot operation
159    SnapshotReferenceUtil.verifySnapshot(conf, fs, manifest);
160  }
161
162  private void addRegionToSnapshot(SnapshotProtos.SnapshotDescription snapshot, HRegion region,
163    SnapshotManifest manifest) throws Exception {
164    LOG.info("Adding region to snapshot: " + region.getRegionInfo().getRegionNameAsString());
165    Path workingDir = SnapshotDescriptionUtils.getWorkingSnapshotDir(snapshot, rootDir, conf);
166    SnapshotManifest.RegionVisitor visitor = createRegionVisitorWithDelay(snapshot, workingDir);
167    manifest.addRegion(region, visitor);
168    LOG.info("Added the region to snapshot: " + region.getRegionInfo().getRegionNameAsString());
169  }
170
171  private SnapshotManifest.RegionVisitor
172    createRegionVisitorWithDelay(SnapshotProtos.SnapshotDescription desc, Path workingDir) {
173    return new SnapshotManifestV2.ManifestBuilder(conf, fs, workingDir) {
174      @Override
175      public void storeFile(final SnapshotProtos.SnapshotRegionManifest.Builder region,
176        final SnapshotProtos.SnapshotRegionManifest.FamilyFiles.Builder family,
177        final StoreFileInfo storeFile) throws IOException {
178        try {
179          LOG.debug("Introducing delay before adding store file to manifest");
180          Thread.sleep(2000);
181        } catch (InterruptedException ex) {
182          LOG.error("Interrupted due to error: " + ex);
183        }
184        super.storeFile(region, family, storeFile);
185      }
186    };
187  }
188
189  private Table setupTable(TableName tableName) throws Exception {
190    TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableName);
191    // Flush many files, but do not compact immediately
192    // Make sure that the region does not split
193    builder.setMemStoreFlushSize(5000)
194      .setRegionSplitPolicyClassName(ConstantSizeRegionSplitPolicy.class.getName())
195      .setMaxFileSize(100 * 1024 * 1024).setValue("hbase.hstore.compactionThreshold", "250");
196
197    TableDescriptor td = builder.build();
198    byte[] fam = Bytes.toBytes("fam");
199    Table table = TEST_UTIL.createTable(td, new byte[][] { fam }, TEST_UTIL.getConfiguration());
200    TEST_UTIL.loadTable(table, fam);
201    return table;
202  }
203}