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.
102   * The idea is to slow down the process of adding a store file to the manifest while
103   * triggering compactions on the region, allowing the store files to be marked for archival while
104   * snapshot operation is running.
105   * This test checks for the correct behavior in such a case that the compacted files should
106   * not be moved around if a snapshot operation is in progress.
107   * See HBASE-18398
108   */
109  @Test
110  public void testAddRegionWithCompactions() throws Exception {
111    final TableName tableName = TableName.valueOf("test_table");
112    Table table = setupTable(tableName);
113
114    List<HRegion> hRegions = TEST_UTIL.getHBaseCluster().getRegions(tableName);
115
116    final SnapshotProtos.SnapshotDescription snapshot =
117        SnapshotProtos.SnapshotDescription.newBuilder()
118        .setTable(tableName.getNameAsString())
119        .setType(SnapshotProtos.SnapshotDescription.Type.FLUSH)
120        .setName("test_table_snapshot")
121        .setVersion(SnapshotManifestV2.DESCRIPTOR_VERSION)
122        .build();
123    ForeignExceptionDispatcher monitor = new ForeignExceptionDispatcher(snapshot.getName());
124
125    final HRegion region = spy(hRegions.get(0));
126
127    Path workingDir = SnapshotDescriptionUtils.getWorkingSnapshotDir(snapshot, rootDir, conf);
128    final SnapshotManifest manifest =
129        SnapshotManifest.create(conf, fs, workingDir, snapshot, monitor);
130    manifest.addTableDescriptor(table.getTableDescriptor());
131
132    if (!fs.exists(workingDir)) {
133      fs.mkdirs(workingDir);
134    }
135    assertTrue(fs.exists(workingDir));
136    SnapshotDescriptionUtils.writeSnapshotInfo(snapshot, workingDir, fs);
137
138    doAnswer(__ -> {
139      addRegionToSnapshot(snapshot, region, manifest);
140      return null;
141    }).when(region).addRegionToSnapshot(snapshot, monitor);
142
143    FlushSnapshotSubprocedure.RegionSnapshotTask snapshotTask =
144        new FlushSnapshotSubprocedure.RegionSnapshotTask(region, snapshot, true, monitor);
145    ExecutorService executor = Executors.newFixedThreadPool(1);
146    Future f = executor.submit(snapshotTask);
147
148    // Trigger major compaction and wait for snaphot operation to finish
149    LOG.info("Starting major compaction");
150    region.compact(true);
151    LOG.info("Finished major compaction");
152    f.get();
153
154    // Consolidate region manifests into a single snapshot manifest
155    manifest.consolidate();
156
157    // Make sure that the region manifest exists, which means the snapshot operation succeeded
158    assertNotNull(manifest.getRegionManifests());
159    // Sanity check, there should be only one region
160    assertEquals(1, manifest.getRegionManifests().size());
161
162    // Make sure that no files went missing after the snapshot operation
163    SnapshotReferenceUtil.verifySnapshot(conf, fs, manifest);
164  }
165
166  private void addRegionToSnapshot(SnapshotProtos.SnapshotDescription snapshot,
167      HRegion region, SnapshotManifest manifest) throws Exception {
168    LOG.info("Adding region to snapshot: " + region.getRegionInfo().getRegionNameAsString());
169    Path workingDir = SnapshotDescriptionUtils.getWorkingSnapshotDir(snapshot, rootDir, conf);
170    SnapshotManifest.RegionVisitor visitor = createRegionVisitorWithDelay(snapshot, workingDir);
171    manifest.addRegion(region, visitor);
172    LOG.info("Added the region to snapshot: " + region.getRegionInfo().getRegionNameAsString());
173  }
174
175  private SnapshotManifest.RegionVisitor createRegionVisitorWithDelay(
176      SnapshotProtos.SnapshotDescription desc, Path workingDir) {
177    return new SnapshotManifestV2.ManifestBuilder(conf, fs, workingDir) {
178      @Override
179      public void storeFile(final SnapshotProtos.SnapshotRegionManifest.Builder region,
180          final SnapshotProtos.SnapshotRegionManifest.FamilyFiles.Builder family,
181          final StoreFileInfo storeFile) throws IOException {
182        try {
183          LOG.debug("Introducing delay before adding store file to manifest");
184          Thread.sleep(2000);
185        } catch (InterruptedException ex) {
186          LOG.error("Interrupted due to error: " + ex);
187        }
188        super.storeFile(region, family, storeFile);
189      }
190    };
191  }
192
193  private Table setupTable(TableName tableName) throws Exception {
194    TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableName);
195    // Flush many files, but do not compact immediately
196    // Make sure that the region does not split
197    builder
198        .setMemStoreFlushSize(5000)
199        .setRegionSplitPolicyClassName(ConstantSizeRegionSplitPolicy.class.getName())
200        .setMaxFileSize(100 * 1024 * 1024)
201        .setValue("hbase.hstore.compactionThreshold", "250");
202
203    TableDescriptor td = builder.build();
204    byte[] fam = Bytes.toBytes("fam");
205    Table table = TEST_UTIL.createTable(td, new byte[][] {fam},
206        TEST_UTIL.getConfiguration());
207    TEST_UTIL.loadTable(table, fam);
208    return table;
209  }
210}