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