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.assertThrows;
022import static org.junit.Assert.assertTrue;
023
024import java.io.IOException;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FileSystem;
030import org.apache.hadoop.fs.LocatedFileStatus;
031import org.apache.hadoop.fs.Path;
032import org.apache.hadoop.fs.RemoteIterator;
033import org.apache.hadoop.hbase.HBaseClassTestRule;
034import org.apache.hadoop.hbase.HBaseTestingUtil;
035import org.apache.hadoop.hbase.HConstants;
036import org.apache.hadoop.hbase.MetaTableAccessor;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.client.Put;
039import org.apache.hadoop.hbase.client.RegionInfo;
040import org.apache.hadoop.hbase.client.ResultScanner;
041import org.apache.hadoop.hbase.client.Scan;
042import org.apache.hadoop.hbase.client.SnapshotType;
043import org.apache.hadoop.hbase.client.Table;
044import org.apache.hadoop.hbase.client.TableDescriptor;
045import org.apache.hadoop.hbase.errorhandling.ForeignExceptionDispatcher;
046import org.apache.hadoop.hbase.io.HFileLink;
047import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
048import org.apache.hadoop.hbase.master.assignment.MergeTableRegionsProcedure;
049import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
050import org.apache.hadoop.hbase.mob.MobUtils;
051import org.apache.hadoop.hbase.monitoring.MonitoredTask;
052import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
053import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
054import org.apache.hadoop.hbase.regionserver.HRegion;
055import org.apache.hadoop.hbase.regionserver.HRegionServer;
056import org.apache.hadoop.hbase.regionserver.StoreFileInfo;
057import org.apache.hadoop.hbase.snapshot.SnapshotTestingUtils.SnapshotMock;
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.util.CommonFSUtils;
062import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
063import org.apache.hadoop.hbase.util.FSTableDescriptors;
064import org.apache.hadoop.hbase.wal.WALSplitUtil;
065import org.junit.After;
066import org.junit.AfterClass;
067import org.junit.Assert;
068import org.junit.Before;
069import org.junit.BeforeClass;
070import org.junit.ClassRule;
071import org.junit.Test;
072import org.junit.experimental.categories.Category;
073import org.mockito.Mockito;
074import org.slf4j.Logger;
075import org.slf4j.LoggerFactory;
076
077import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
078
079/**
080 * Test the restore/clone operation from a file-system point of view.
081 */
082@Category({ RegionServerTests.class, MediumTests.class })
083public class TestRestoreSnapshotHelper {
084
085  @ClassRule
086  public static final HBaseClassTestRule CLASS_RULE =
087    HBaseClassTestRule.forClass(TestRestoreSnapshotHelper.class);
088
089  private static final Logger LOG = LoggerFactory.getLogger(TestRestoreSnapshotHelper.class);
090
091  protected final static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
092  protected final static String TEST_HFILE = "abc";
093
094  protected Configuration conf;
095  protected Path archiveDir;
096  protected FileSystem fs;
097  protected Path rootDir;
098
099  protected void setupConf(Configuration conf) {
100  }
101
102  @BeforeClass
103  public static void setupCluster() throws Exception {
104    TEST_UTIL.getConfiguration().setInt(AssignmentManager.ASSIGN_MAX_ATTEMPTS, 3);
105    TEST_UTIL.startMiniCluster();
106  }
107
108  @AfterClass
109  public static void tearDownCluster() throws Exception {
110    TEST_UTIL.shutdownMiniCluster();
111  }
112
113  @Before
114  public void setup() throws Exception {
115    rootDir = TEST_UTIL.getDataTestDir("testRestore");
116    archiveDir = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY);
117    fs = TEST_UTIL.getTestFileSystem();
118    conf = TEST_UTIL.getConfiguration();
119    setupConf(conf);
120    CommonFSUtils.setRootDir(conf, rootDir);
121    // Turn off balancer so it doesn't cut in and mess up our placements.
122    TEST_UTIL.getAdmin().balancerSwitch(false, true);
123  }
124
125  @After
126  public void tearDown() throws Exception {
127    fs.delete(TEST_UTIL.getDataTestDir(), true);
128  }
129
130  protected SnapshotMock createSnapshotMock() throws IOException {
131    return new SnapshotMock(TEST_UTIL.getConfiguration(), fs, rootDir);
132  }
133
134  @Test
135  public void testRestore() throws IOException {
136    restoreAndVerify("snapshot", "testRestore");
137  }
138
139  @Test
140  public void testRestoreWithNamespace() throws IOException {
141    restoreAndVerify("snapshot", "namespace1:testRestoreWithNamespace");
142  }
143
144  @Test
145  public void testNoHFileLinkInRootDir() throws IOException {
146    rootDir = TEST_UTIL.getDefaultRootDirPath();
147    CommonFSUtils.setRootDir(conf, rootDir);
148    fs = rootDir.getFileSystem(conf);
149
150    TableName tableName = TableName.valueOf("testNoHFileLinkInRootDir");
151    String snapshotName = tableName.getNameAsString() + "-snapshot";
152    createTableAndSnapshot(tableName, snapshotName);
153
154    Path restoreDir = new Path("/hbase/.tmp-restore");
155    RestoreSnapshotHelper.copySnapshotForScanner(conf, fs, rootDir, restoreDir, snapshotName);
156    checkNoHFileLinkInTableDir(tableName);
157  }
158
159  @Test
160  public void testSkipReplayAndUpdateSeqId() throws Exception {
161    rootDir = TEST_UTIL.getDefaultRootDirPath();
162    CommonFSUtils.setRootDir(conf, rootDir);
163    TableName tableName = TableName.valueOf("testSkipReplayAndUpdateSeqId");
164    String snapshotName = "testSkipReplayAndUpdateSeqId";
165    createTableAndSnapshot(tableName, snapshotName);
166    // put some data in the table
167    Table table = TEST_UTIL.getConnection().getTable(tableName);
168    TEST_UTIL.loadTable(table, Bytes.toBytes("A"));
169
170    Configuration conf = TEST_UTIL.getConfiguration();
171    Path rootDir = CommonFSUtils.getRootDir(conf);
172    Path restoreDir = new Path("/hbase/.tmp-restore/testScannerWithRestoreScanner2");
173    // restore snapshot.
174    final RestoreSnapshotHelper.RestoreMetaChanges meta =
175      RestoreSnapshotHelper.copySnapshotForScanner(conf, fs, rootDir, restoreDir, snapshotName);
176    TableDescriptor htd = meta.getTableDescriptor();
177    final List<RegionInfo> restoredRegions = meta.getRegionsToAdd();
178    for (RegionInfo restoredRegion : restoredRegions) {
179      // open restored region
180      HRegion region = HRegion.newHRegion(CommonFSUtils.getTableDir(restoreDir, tableName), null,
181        fs, conf, restoredRegion, htd, null);
182      // set restore flag
183      region.setRestoredRegion(true);
184      region.initialize();
185      Path recoveredEdit =
186        CommonFSUtils.getWALRegionDir(conf, tableName, region.getRegionInfo().getEncodedName());
187      long maxSeqId = WALSplitUtil.getMaxRegionSequenceId(fs, recoveredEdit);
188
189      // open restored region without set restored flag
190      HRegion region2 = HRegion.newHRegion(CommonFSUtils.getTableDir(restoreDir, tableName), null,
191        fs, conf, restoredRegion, htd, null);
192      region2.initialize();
193      long maxSeqId2 = WALSplitUtil.getMaxRegionSequenceId(fs, recoveredEdit);
194      Assert.assertTrue(maxSeqId2 > maxSeqId);
195    }
196  }
197
198  @Test
199  public void testCopyExpiredSnapshotForScanner() throws IOException, InterruptedException {
200    rootDir = TEST_UTIL.getDefaultRootDirPath();
201    CommonFSUtils.setRootDir(conf, rootDir);
202    TableName tableName = TableName.valueOf("testCopyExpiredSnapshotForScanner");
203    String snapshotName = tableName.getNameAsString() + "-snapshot";
204    Path restoreDir = new Path("/hbase/.tmp-expired-snapshot/copySnapshotDest");
205    // create table and put some data into the table
206    byte[] columnFamily = Bytes.toBytes("A");
207    Table table = TEST_UTIL.createTable(tableName, columnFamily);
208    TEST_UTIL.loadTable(table, columnFamily);
209    // create snapshot with ttl = 10 sec
210    Map<String, Object> properties = new HashMap<>();
211    properties.put("TTL", 10);
212    org.apache.hadoop.hbase.client.SnapshotDescription snapshotDesc =
213      new org.apache.hadoop.hbase.client.SnapshotDescription(snapshotName, tableName,
214        SnapshotType.FLUSH, null, EnvironmentEdgeManager.currentTime(), -1, properties);
215    TEST_UTIL.getAdmin().snapshot(snapshotDesc);
216    boolean isExist = TEST_UTIL.getAdmin().listSnapshots().stream()
217      .anyMatch(ele -> snapshotName.equals(ele.getName()));
218    assertTrue(isExist);
219    int retry = 6;
220    while (
221      !SnapshotDescriptionUtils.isExpiredSnapshot(snapshotDesc.getTtl(),
222        snapshotDesc.getCreationTime(), EnvironmentEdgeManager.currentTime()) && retry > 0
223    ) {
224      retry--;
225      Thread.sleep(10 * 1000);
226    }
227    boolean isExpiredSnapshot = SnapshotDescriptionUtils.isExpiredSnapshot(snapshotDesc.getTtl(),
228      snapshotDesc.getCreationTime(), EnvironmentEdgeManager.currentTime());
229    assertTrue(isExpiredSnapshot);
230    assertThrows(SnapshotTTLExpiredException.class, () -> RestoreSnapshotHelper
231      .copySnapshotForScanner(conf, fs, rootDir, restoreDir, snapshotName));
232  }
233
234  /**
235   * Test scenario for HBASE-29346, which addresses the issue where restoring snapshots after region
236   * merge operations could lead to missing store file references, potentially resulting in data
237   * loss.
238   * <p>
239   * This test performs the following steps:
240   * </p>
241   * <ol>
242   * <li>Creates a table with multiple regions.</li>
243   * <li>Inserts data into each region and flushes to create store files.</li>
244   * <li>Takes snapshot of the table and performs restore.</li>
245   * <li>Disable compactions, merge regions, create a new snapshot, and restore that snapshot on the
246   * same restore path.</li>
247   * <li>Verifies data integrity by scanning all data post region re-open.</li>
248   * </ol>
249   */
250  @Test
251  public void testMultiSnapshotRestoreWithMerge() throws IOException, InterruptedException {
252    rootDir = TEST_UTIL.getDefaultRootDirPath();
253    CommonFSUtils.setRootDir(conf, rootDir);
254    TableName tableName = TableName.valueOf("testMultiSnapshotRestoreWithMerge");
255    Path restoreDir = new Path("/hbase/.tmp-snapshot/restore-snapshot-dest");
256
257    byte[] columnFamily = Bytes.toBytes("A");
258    Table table = TEST_UTIL.createTable(tableName, new byte[][] { columnFamily },
259      new byte[][] { new byte[] { 'b' }, new byte[] { 'd' } });
260    Put put1 = new Put(Bytes.toBytes("a")); // Region 1: [-∞, b)
261    put1.addColumn(columnFamily, Bytes.toBytes("q"), Bytes.toBytes("val1"));
262    table.put(put1);
263    Put put2 = new Put(Bytes.toBytes("b")); // Region 2: [b, d)
264    put2.addColumn(columnFamily, Bytes.toBytes("q"), Bytes.toBytes("val2"));
265    table.put(put2);
266    Put put3 = new Put(Bytes.toBytes("d")); // Region 3: [d, +∞)
267    put3.addColumn(columnFamily, Bytes.toBytes("q"), Bytes.toBytes("val3"));
268    table.put(put3);
269
270    TEST_UTIL.getAdmin().flush(tableName);
271
272    String snapshotOne = tableName.getNameAsString() + "-snapshot-one";
273    createAndAssertSnapshot(tableName, snapshotOne);
274    RestoreSnapshotHelper.copySnapshotForScanner(conf, fs, rootDir, restoreDir, snapshotOne);
275    flipCompactions(false);
276    mergeRegions(tableName, 2);
277    String snapshotTwo = tableName.getNameAsString() + "-snapshot-two";
278    createAndAssertSnapshot(tableName, snapshotTwo);
279    RestoreSnapshotHelper.copySnapshotForScanner(conf, fs, rootDir, restoreDir, snapshotTwo);
280    flipCompactions(true);
281
282    TEST_UTIL.getAdmin().disableTable(tableName);
283    TEST_UTIL.getAdmin().enableTable(tableName);
284    try (ResultScanner scanner = table.getScanner(new Scan())) {
285      assertEquals(3, scanner.next(4).length);
286    }
287    String snapshotThree = tableName.getNameAsString() + "-snapshot-three";
288    createAndAssertSnapshot(tableName, snapshotThree);
289  }
290
291  private void createAndAssertSnapshot(TableName tableName, String snapshotName)
292    throws SnapshotCreationException, IllegalArgumentException, IOException {
293    org.apache.hadoop.hbase.client.SnapshotDescription snapshotDescOne =
294      new org.apache.hadoop.hbase.client.SnapshotDescription(snapshotName, tableName,
295        SnapshotType.FLUSH, null, EnvironmentEdgeManager.currentTime(), -1);
296    TEST_UTIL.getAdmin().snapshot(snapshotDescOne);
297    boolean isExist = TEST_UTIL.getAdmin().listSnapshots().stream()
298      .anyMatch(ele -> snapshotName.equals(ele.getName()));
299    assertTrue(isExist);
300
301  }
302
303  private void flipCompactions(boolean isEnable) {
304    int numLiveRegionServers = TEST_UTIL.getHBaseCluster().getNumLiveRegionServers();
305    for (int serverNumber = 0; serverNumber < numLiveRegionServers; serverNumber++) {
306      HRegionServer regionServer = TEST_UTIL.getHBaseCluster().getRegionServer(serverNumber);
307      regionServer.getCompactSplitThread().setCompactionsEnabled(isEnable);
308    }
309
310  }
311
312  private void mergeRegions(TableName tableName, int mergeCount) throws IOException {
313    List<RegionInfo> ris = MetaTableAccessor.getTableRegions(TEST_UTIL.getConnection(), tableName);
314    int originalRegionCount = ris.size();
315    assertTrue(originalRegionCount > mergeCount);
316    RegionInfo[] regionsToMerge = ris.subList(0, mergeCount).toArray(new RegionInfo[] {});
317    final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor();
318    MergeTableRegionsProcedure proc =
319      new MergeTableRegionsProcedure(procExec.getEnvironment(), regionsToMerge, true);
320    long procId = procExec.submitProcedure(proc);
321    ProcedureTestingUtility.waitProcedure(procExec, procId);
322    ProcedureTestingUtility.assertProcNotFailed(procExec, procId);
323    MetaTableAccessor.fullScanMetaAndPrint(TEST_UTIL.getConnection());
324    assertEquals(originalRegionCount - mergeCount + 1,
325      MetaTableAccessor.getTableRegions(TEST_UTIL.getConnection(), tableName).size());
326  }
327
328  private ProcedureExecutor<MasterProcedureEnv> getMasterProcedureExecutor() {
329    return TEST_UTIL.getHBaseCluster().getMaster().getMasterProcedureExecutor();
330  }
331
332  protected void createTableAndSnapshot(TableName tableName, String snapshotName)
333    throws IOException {
334    byte[] column = Bytes.toBytes("A");
335    Table table = TEST_UTIL.createTable(tableName, column, 2);
336    TEST_UTIL.loadTable(table, column);
337    TEST_UTIL.getAdmin().snapshot(snapshotName, tableName);
338  }
339
340  private void checkNoHFileLinkInTableDir(TableName tableName) throws IOException {
341    Path[] tableDirs = new Path[] { CommonFSUtils.getTableDir(rootDir, tableName),
342      CommonFSUtils.getTableDir(new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY), tableName),
343      CommonFSUtils.getTableDir(MobUtils.getMobHome(rootDir), tableName) };
344    for (Path tableDir : tableDirs) {
345      Assert.assertFalse(hasHFileLink(tableDir));
346    }
347  }
348
349  private boolean hasHFileLink(Path tableDir) throws IOException {
350    if (fs.exists(tableDir)) {
351      RemoteIterator<LocatedFileStatus> iterator = fs.listFiles(tableDir, true);
352      while (iterator.hasNext()) {
353        LocatedFileStatus fileStatus = iterator.next();
354        if (fileStatus.isFile() && HFileLink.isHFileLink(fileStatus.getPath())) {
355          return true;
356        }
357      }
358    }
359    return false;
360  }
361
362  private void restoreAndVerify(final String snapshotName, final String tableName)
363    throws IOException {
364    // Test Rolling-Upgrade like Snapshot.
365    // half machines writing using v1 and the others using v2 format.
366    SnapshotMock snapshotMock = createSnapshotMock();
367    SnapshotMock.SnapshotBuilder builder = snapshotMock.createSnapshotV2("snapshot", tableName);
368    builder.addRegionV1();
369    builder.addRegionV2();
370    builder.addRegionV2();
371    builder.addRegionV1();
372    Path snapshotDir = builder.commit();
373    TableDescriptor htd = builder.getTableDescriptor();
374    SnapshotDescription desc = builder.getSnapshotDescription();
375
376    // Test clone a snapshot
377    TableDescriptor htdClone = snapshotMock.createHtd("testtb-clone");
378    testRestore(snapshotDir, desc, htdClone);
379    verifyRestore(rootDir, htd, htdClone);
380
381    // Test clone a clone ("link to link")
382    SnapshotDescription cloneDesc =
383      SnapshotDescription.newBuilder().setName("cloneSnapshot").setTable("testtb-clone").build();
384    Path cloneDir = CommonFSUtils.getTableDir(rootDir, htdClone.getTableName());
385    TableDescriptor htdClone2 = snapshotMock.createHtd("testtb-clone2");
386    testRestore(cloneDir, cloneDesc, htdClone2);
387    verifyRestore(rootDir, htd, htdClone2);
388  }
389
390  private void verifyRestore(final Path rootDir, final TableDescriptor sourceHtd,
391    final TableDescriptor htdClone) throws IOException {
392    List<String> files = SnapshotTestingUtils.listHFileNames(fs,
393      CommonFSUtils.getTableDir(rootDir, htdClone.getTableName()));
394    assertEquals(12, files.size());
395    for (int i = 0; i < files.size(); i += 2) {
396      String linkFile = files.get(i);
397      String refFile = files.get(i + 1);
398      assertTrue(linkFile + " should be a HFileLink", HFileLink.isHFileLink(linkFile));
399      assertTrue(refFile + " should be a Referene", StoreFileInfo.isReference(refFile));
400      assertEquals(sourceHtd.getTableName(), HFileLink.getReferencedTableName(linkFile));
401      Path refPath = getReferredToFile(refFile);
402      LOG.debug("get reference name for file " + refFile + " = " + refPath);
403      assertTrue(refPath.getName() + " should be a HFileLink",
404        HFileLink.isHFileLink(refPath.getName()));
405      assertEquals(linkFile, refPath.getName());
406    }
407  }
408
409  /**
410   * Execute the restore operation
411   * @param snapshotDir The snapshot directory to use as "restore source"
412   * @param sd          The snapshot descriptor
413   * @param htdClone    The HTableDescriptor of the table to restore/clone.
414   */
415  private void testRestore(final Path snapshotDir, final SnapshotDescription sd,
416    final TableDescriptor htdClone) throws IOException {
417    LOG.debug("pre-restore table=" + htdClone.getTableName() + " snapshot=" + snapshotDir);
418    CommonFSUtils.logFileSystemState(fs, rootDir, LOG);
419
420    new FSTableDescriptors(conf).createTableDescriptor(htdClone);
421    RestoreSnapshotHelper helper = getRestoreHelper(rootDir, snapshotDir, sd, htdClone);
422    helper.restoreHdfsRegions();
423
424    LOG.debug("post-restore table=" + htdClone.getTableName() + " snapshot=" + snapshotDir);
425    CommonFSUtils.logFileSystemState(fs, rootDir, LOG);
426  }
427
428  /**
429   * Initialize the restore helper, based on the snapshot and table information provided.
430   */
431  private RestoreSnapshotHelper getRestoreHelper(final Path rootDir, final Path snapshotDir,
432    final SnapshotDescription sd, final TableDescriptor htdClone) throws IOException {
433    ForeignExceptionDispatcher monitor = Mockito.mock(ForeignExceptionDispatcher.class);
434    MonitoredTask status = Mockito.mock(MonitoredTask.class);
435
436    SnapshotManifest manifest = SnapshotManifest.open(conf, fs, snapshotDir, sd);
437    return new RestoreSnapshotHelper(conf, fs, manifest, htdClone, rootDir, monitor, status);
438  }
439
440  private Path getReferredToFile(final String referenceName) {
441    Path fakeBasePath = new Path(new Path("table", "region"), "cf");
442    return StoreFileInfo.getReferredToFile(new Path(fakeBasePath, referenceName));
443  }
444}