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.backup;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertNotNull;
023import static org.junit.Assert.assertTrue;
024import static org.junit.Assert.fail;
025import static org.mockito.ArgumentMatchers.any;
026import static org.mockito.Mockito.mock;
027import static org.mockito.Mockito.never;
028import static org.mockito.Mockito.times;
029import static org.mockito.Mockito.verify;
030import static org.mockito.Mockito.when;
031
032import java.io.IOException;
033import java.security.PrivilegedExceptionAction;
034import java.util.ArrayList;
035import java.util.Collection;
036import java.util.Collections;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.Objects;
041import java.util.stream.Collectors;
042import org.apache.hadoop.conf.Configuration;
043import org.apache.hadoop.fs.FileStatus;
044import org.apache.hadoop.fs.FileSystem;
045import org.apache.hadoop.fs.Path;
046import org.apache.hadoop.fs.PathFilter;
047import org.apache.hadoop.hbase.ChoreService;
048import org.apache.hadoop.hbase.HBaseClassTestRule;
049import org.apache.hadoop.hbase.HBaseTestingUtil;
050import org.apache.hadoop.hbase.HConstants;
051import org.apache.hadoop.hbase.Stoppable;
052import org.apache.hadoop.hbase.TableName;
053import org.apache.hadoop.hbase.client.Admin;
054import org.apache.hadoop.hbase.client.RegionInfo;
055import org.apache.hadoop.hbase.client.Table;
056import org.apache.hadoop.hbase.master.HMaster;
057import org.apache.hadoop.hbase.master.cleaner.DirScanPool;
058import org.apache.hadoop.hbase.master.cleaner.HFileCleaner;
059import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy;
060import org.apache.hadoop.hbase.regionserver.HRegion;
061import org.apache.hadoop.hbase.regionserver.HRegionServer;
062import org.apache.hadoop.hbase.regionserver.HStoreFile;
063import org.apache.hadoop.hbase.testclassification.LargeTests;
064import org.apache.hadoop.hbase.testclassification.MiscTests;
065import org.apache.hadoop.hbase.util.Bytes;
066import org.apache.hadoop.hbase.util.CommonFSUtils;
067import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
068import org.apache.hadoop.hbase.util.FSUtils;
069import org.apache.hadoop.hbase.util.HFileArchiveTestingUtil;
070import org.apache.hadoop.hbase.util.HFileArchiveUtil;
071import org.apache.hadoop.hbase.util.StoppableImplementation;
072import org.apache.hadoop.security.UserGroupInformation;
073import org.junit.After;
074import org.junit.AfterClass;
075import org.junit.Assert;
076import org.junit.BeforeClass;
077import org.junit.ClassRule;
078import org.junit.Rule;
079import org.junit.Test;
080import org.junit.experimental.categories.Category;
081import org.junit.rules.TestName;
082import org.mockito.ArgumentCaptor;
083import org.slf4j.Logger;
084import org.slf4j.LoggerFactory;
085
086/**
087 * Test that the {@link HFileArchiver} correctly removes all the parts of a region when cleaning up
088 * a region
089 */
090@Category({ LargeTests.class, MiscTests.class })
091public class TestHFileArchiving {
092
093  @ClassRule
094  public static final HBaseClassTestRule CLASS_RULE =
095    HBaseClassTestRule.forClass(TestHFileArchiving.class);
096
097  private static final Logger LOG = LoggerFactory.getLogger(TestHFileArchiving.class);
098  private static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
099  private static final byte[] TEST_FAM = Bytes.toBytes("fam");
100
101  private static DirScanPool POOL;
102  @Rule
103  public TestName name = new TestName();
104
105  /**
106   * Setup the config for the cluster
107   */
108  @BeforeClass
109  public static void setupCluster() throws Exception {
110    setupConf(UTIL.getConfiguration());
111    UTIL.startMiniCluster();
112
113    // We don't want the cleaner to remove files. The tests do that.
114    UTIL.getMiniHBaseCluster().getMaster().getHFileCleaner().cancel(true);
115
116    POOL = DirScanPool.getHFileCleanerScanPool(UTIL.getConfiguration());
117  }
118
119  private static void setupConf(Configuration conf) {
120    // disable the ui
121    conf.setInt("hbase.regionsever.info.port", -1);
122    // drop the memstore size so we get flushes
123    conf.setInt("hbase.hregion.memstore.flush.size", 25000);
124    // disable major compactions
125    conf.setInt(HConstants.MAJOR_COMPACTION_PERIOD, 0);
126
127    // prevent aggressive region split
128    conf.set(HConstants.HBASE_REGION_SPLIT_POLICY_KEY,
129      ConstantSizeRegionSplitPolicy.class.getName());
130  }
131
132  @After
133  public void tearDown() throws Exception {
134    // cleanup the archive directory
135    clearArchiveDirectory();
136  }
137
138  @AfterClass
139  public static void cleanupTest() throws Exception {
140    UTIL.shutdownMiniCluster();
141    POOL.shutdownNow();
142  }
143
144  @Test
145  public void testArchiveStoreFilesDifferentFileSystemsWallWithSchemaPlainRoot() throws Exception {
146    String walDir = "mockFS://mockFSAuthority:9876/mockDir/wals/";
147    String baseDir = CommonFSUtils.getRootDir(UTIL.getConfiguration()).toString() + "/";
148    testArchiveStoreFilesDifferentFileSystems(walDir, baseDir, HFileArchiver::archiveStoreFiles);
149  }
150
151  @Test
152  public void testArchiveStoreFilesDifferentFileSystemsWallNullPlainRoot() throws Exception {
153    String baseDir = CommonFSUtils.getRootDir(UTIL.getConfiguration()).toString() + "/";
154    testArchiveStoreFilesDifferentFileSystems(null, baseDir, HFileArchiver::archiveStoreFiles);
155  }
156
157  @Test
158  public void testArchiveStoreFilesDifferentFileSystemsWallAndRootSame() throws Exception {
159    String baseDir = CommonFSUtils.getRootDir(UTIL.getConfiguration()).toString() + "/";
160    testArchiveStoreFilesDifferentFileSystems("/hbase/wals/", baseDir,
161      HFileArchiver::archiveStoreFiles);
162  }
163
164  @Test
165  public void testArchiveStoreFilesDifferentFileSystemsFileAlreadyArchived() throws Exception {
166    String baseDir = CommonFSUtils.getRootDir(UTIL.getConfiguration()).toString() + "/";
167    testArchiveStoreFilesDifferentFileSystems("/hbase/wals/", baseDir, true, false, false,
168      HFileArchiver::archiveStoreFiles);
169  }
170
171  @Test
172  public void testArchiveStoreFilesDifferentFileSystemsArchiveFileMatchCurrent() throws Exception {
173    String baseDir = CommonFSUtils.getRootDir(UTIL.getConfiguration()).toString() + "/";
174    testArchiveStoreFilesDifferentFileSystems("/hbase/wals/", baseDir, true, true, false,
175      HFileArchiver::archiveStoreFiles);
176  }
177
178  @Test(expected = IOException.class)
179  public void testArchiveStoreFilesDifferentFileSystemsArchiveFileMismatch() throws Exception {
180    String baseDir = CommonFSUtils.getRootDir(UTIL.getConfiguration()).toString() + "/";
181    testArchiveStoreFilesDifferentFileSystems("/hbase/wals/", baseDir, true, true, true,
182      HFileArchiver::archiveStoreFiles);
183  }
184
185  private void testArchiveStoreFilesDifferentFileSystems(String walDir, String expectedBase,
186    ArchivingFunction<Configuration, FileSystem, RegionInfo, Path, byte[],
187      Collection<HStoreFile>> archivingFunction)
188    throws IOException {
189    testArchiveStoreFilesDifferentFileSystems(walDir, expectedBase, false, true, false,
190      archivingFunction);
191  }
192
193  private void testArchiveStoreFilesDifferentFileSystems(String walDir, String expectedBase,
194    boolean archiveFileExists, boolean sourceFileExists, boolean archiveFileDifferentLength,
195    ArchivingFunction<Configuration, FileSystem, RegionInfo, Path, byte[],
196      Collection<HStoreFile>> archivingFunction)
197    throws IOException {
198    FileSystem mockedFileSystem = mock(FileSystem.class);
199    Configuration conf = new Configuration(UTIL.getConfiguration());
200    if (walDir != null) {
201      conf.set(CommonFSUtils.HBASE_WAL_DIR, walDir);
202    }
203    when(mockedFileSystem.getScheme()).thenReturn("mockFS");
204    when(mockedFileSystem.mkdirs(any())).thenReturn(true);
205    HashMap<Path, Boolean> existsTracker = new HashMap<>();
206    Path filePath = new Path("/mockDir/wals/mockFile");
207    String expectedDir = expectedBase
208      + "archive/data/default/mockTable/mocked-region-encoded-name/testfamily/mockFile";
209    existsTracker.put(new Path(expectedDir), archiveFileExists);
210    existsTracker.put(filePath, sourceFileExists);
211    when(mockedFileSystem.exists(any()))
212      .thenAnswer(invocation -> existsTracker.getOrDefault((Path) invocation.getArgument(0), true));
213    FileStatus mockedStatus = mock(FileStatus.class);
214    when(mockedStatus.getLen()).thenReturn(12L).thenReturn(archiveFileDifferentLength ? 34L : 12L);
215    when(mockedFileSystem.getFileStatus(any())).thenReturn(mockedStatus);
216    RegionInfo mockedRegion = mock(RegionInfo.class);
217    TableName tableName = TableName.valueOf("mockTable");
218    when(mockedRegion.getTable()).thenReturn(tableName);
219    when(mockedRegion.getEncodedName()).thenReturn("mocked-region-encoded-name");
220    Path tableDir = new Path("mockFS://mockDir/tabledir");
221    byte[] family = Bytes.toBytes("testfamily");
222    HStoreFile mockedFile = mock(HStoreFile.class);
223    List<HStoreFile> list = new ArrayList<>();
224    list.add(mockedFile);
225    when(mockedFile.getPath()).thenReturn(filePath);
226    when(mockedFileSystem.rename(any(), any())).thenReturn(true);
227    archivingFunction.apply(conf, mockedFileSystem, mockedRegion, tableDir, family, list);
228
229    if (sourceFileExists) {
230      ArgumentCaptor<Path> srcPath = ArgumentCaptor.forClass(Path.class);
231      ArgumentCaptor<Path> destPath = ArgumentCaptor.forClass(Path.class);
232      if (archiveFileExists) {
233        // Verify we renamed the archived file to sideline, and then renamed the source file.
234        verify(mockedFileSystem, times(2)).rename(srcPath.capture(), destPath.capture());
235        assertEquals(expectedDir, srcPath.getAllValues().get(0).toString());
236        assertEquals(filePath, srcPath.getAllValues().get(1));
237        assertEquals(expectedDir, destPath.getAllValues().get(1).toString());
238      } else {
239        // Verify we renamed the source file to the archived file.
240        verify(mockedFileSystem, times(1)).rename(srcPath.capture(), destPath.capture());
241        assertEquals(filePath, srcPath.getAllValues().get(0));
242        assertEquals(expectedDir, destPath.getAllValues().get(0).toString());
243      }
244    } else {
245      if (archiveFileExists) {
246        // Verify we did not rename. No source file with a present archive file should be a no-op.
247        verify(mockedFileSystem, never()).rename(any(), any());
248      } else {
249        fail("Unsupported test conditions: sourceFileExists and archiveFileExists both false.");
250      }
251    }
252  }
253
254  @FunctionalInterface
255  private interface ArchivingFunction<Configuration, FS, Region, Dir, Family, Files> {
256    void apply(Configuration config, FS fs, Region region, Dir dir, Family family, Files files)
257      throws IOException;
258  }
259
260  @Test
261  public void testArchiveRecoveredEditsWalDirNull() throws Exception {
262    testArchiveRecoveredEditsWalDirNullOrSame(null);
263  }
264
265  @Test
266  public void testArchiveRecoveredEditsWalDirSameFsStoreFiles() throws Exception {
267    testArchiveRecoveredEditsWalDirNullOrSame("/wal-dir");
268  }
269
270  private void testArchiveRecoveredEditsWalDirNullOrSame(String walDir) throws Exception {
271    String originalRootDir = UTIL.getConfiguration().get(HConstants.HBASE_DIR);
272    try {
273      String baseDir = "mockFS://mockFSAuthority:9876/hbase/";
274      UTIL.getConfiguration().set(HConstants.HBASE_DIR, baseDir);
275      testArchiveStoreFilesDifferentFileSystems(walDir, baseDir, (conf, fs, region, dir, family,
276        list) -> HFileArchiver.archiveRecoveredEdits(conf, fs, region, family, list));
277    } finally {
278      UTIL.getConfiguration().set(HConstants.HBASE_DIR, originalRootDir);
279    }
280  }
281
282  @Test(expected = IOException.class)
283  public void testArchiveRecoveredEditsWrongFS() throws Exception {
284    String baseDir = CommonFSUtils.getRootDir(UTIL.getConfiguration()).toString() + "/";
285    // Internally, testArchiveStoreFilesDifferentFileSystems will pass a "mockedFS"
286    // to HFileArchiver.archiveRecoveredEdits, but since wal-dir is supposedly on same FS
287    // as root dir it would lead to conflicting FSes and an IOException is expected.
288    testArchiveStoreFilesDifferentFileSystems("/wal-dir", baseDir, (conf, fs, region, dir, family,
289      list) -> HFileArchiver.archiveRecoveredEdits(conf, fs, region, family, list));
290  }
291
292  @Test
293  public void testArchiveRecoveredEditsWalDirDifferentFS() throws Exception {
294    String walDir = "mockFS://mockFSAuthority:9876/mockDir/wals/";
295    testArchiveStoreFilesDifferentFileSystems(walDir, walDir, (conf, fs, region, dir, family,
296      list) -> HFileArchiver.archiveRecoveredEdits(conf, fs, region, family, list));
297  }
298
299  @Test
300  public void testRemoveRegionDirOnArchive() throws Exception {
301    final TableName tableName = TableName.valueOf(name.getMethodName());
302    UTIL.createTable(tableName, TEST_FAM);
303
304    final Admin admin = UTIL.getAdmin();
305
306    // get the current store files for the region
307    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
308    // make sure we only have 1 region serving this table
309    assertEquals(1, servingRegions.size());
310    HRegion region = servingRegions.get(0);
311
312    // and load the table
313    UTIL.loadRegion(region, TEST_FAM);
314
315    // shutdown the table so we can manipulate the files
316    admin.disableTable(tableName);
317
318    FileSystem fs = UTIL.getTestFileSystem();
319
320    // now attempt to depose the region
321    Path rootDir = region.getRegionFileSystem().getTableDir().getParent();
322    Path regionDir = FSUtils.getRegionDirFromRootDir(rootDir, region.getRegionInfo());
323
324    HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo());
325
326    // check for the existence of the archive directory and some files in it
327    Path archiveDir = HFileArchiveTestingUtil.getRegionArchiveDir(UTIL.getConfiguration(), region);
328    assertTrue(fs.exists(archiveDir));
329
330    // check to make sure the store directory was copied
331    FileStatus[] stores = fs.listStatus(archiveDir, new PathFilter() {
332      @Override
333      public boolean accept(Path p) {
334        if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) {
335          return false;
336        }
337        return true;
338      }
339    });
340    assertTrue(stores.length == 1);
341
342    // make sure we archived the store files
343    FileStatus[] storeFiles = fs.listStatus(stores[0].getPath());
344    assertTrue(storeFiles.length > 0);
345
346    // then ensure the region's directory isn't present
347    assertFalse(fs.exists(regionDir));
348
349    UTIL.deleteTable(tableName);
350  }
351
352  /**
353   * Test that the region directory is removed when we archive a region without store files, but
354   * still has hidden files.
355   * @throws IOException throws an IOException if there's problem creating a table or if there's an
356   *                     issue with accessing FileSystem.
357   */
358  @Test
359  public void testDeleteRegionWithNoStoreFiles() throws IOException {
360    final TableName tableName = TableName.valueOf(name.getMethodName());
361    UTIL.createTable(tableName, TEST_FAM);
362
363    // get the current store files for the region
364    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
365    // make sure we only have 1 region serving this table
366    assertEquals(1, servingRegions.size());
367    HRegion region = servingRegions.get(0);
368
369    FileSystem fs = region.getRegionFileSystem().getFileSystem();
370
371    // make sure there are some files in the regiondir
372    Path rootDir = CommonFSUtils.getRootDir(fs.getConf());
373    Path regionDir = FSUtils.getRegionDirFromRootDir(rootDir, region.getRegionInfo());
374    FileStatus[] regionFiles = CommonFSUtils.listStatus(fs, regionDir, null);
375    Assert.assertNotNull("No files in the region directory", regionFiles);
376    if (LOG.isDebugEnabled()) {
377      List<Path> files = new ArrayList<>();
378      for (FileStatus file : regionFiles) {
379        files.add(file.getPath());
380      }
381      LOG.debug("Current files:" + files);
382    }
383    // delete the visible folders so we just have hidden files/folders
384    final PathFilter dirFilter = new FSUtils.DirFilter(fs);
385    PathFilter nonHidden = new PathFilter() {
386      @Override
387      public boolean accept(Path file) {
388        return dirFilter.accept(file) && !file.getName().startsWith(".");
389      }
390    };
391    FileStatus[] storeDirs = CommonFSUtils.listStatus(fs, regionDir, nonHidden);
392    for (FileStatus store : storeDirs) {
393      LOG.debug("Deleting store for test");
394      fs.delete(store.getPath(), true);
395    }
396
397    // then archive the region
398    HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo());
399
400    // and check to make sure the region directoy got deleted
401    assertFalse("Region directory (" + regionDir + "), still exists.", fs.exists(regionDir));
402
403    UTIL.deleteTable(tableName);
404  }
405
406  private List<HRegion> initTableForArchivingRegions(TableName tableName) throws IOException {
407    final byte[][] splitKeys =
408      new byte[][] { Bytes.toBytes("b"), Bytes.toBytes("c"), Bytes.toBytes("d") };
409
410    UTIL.createTable(tableName, TEST_FAM, splitKeys);
411
412    // get the current store files for the regions
413    List<HRegion> regions = UTIL.getHBaseCluster().getRegions(tableName);
414    // make sure we have 4 regions serving this table
415    assertEquals(4, regions.size());
416
417    // and load the table
418    try (Table table = UTIL.getConnection().getTable(tableName)) {
419      UTIL.loadTable(table, TEST_FAM);
420    }
421
422    // disable the table so that we can manipulate the files
423    UTIL.getAdmin().disableTable(tableName);
424
425    return regions;
426  }
427
428  @Test
429  public void testArchiveRegions() throws Exception {
430    final TableName tableName = TableName.valueOf(name.getMethodName());
431    List<HRegion> regions = initTableForArchivingRegions(tableName);
432
433    FileSystem fs = UTIL.getTestFileSystem();
434
435    // now attempt to depose the regions
436    Path rootDir = CommonFSUtils.getRootDir(UTIL.getConfiguration());
437    Path tableDir = CommonFSUtils.getTableDir(rootDir, regions.get(0).getRegionInfo().getTable());
438    List<Path> regionDirList = regions.stream()
439      .map(region -> FSUtils.getRegionDirFromTableDir(tableDir, region.getRegionInfo()))
440      .collect(Collectors.toList());
441
442    HFileArchiver.archiveRegions(UTIL.getConfiguration(), fs, rootDir, tableDir, regionDirList);
443
444    // check for the existence of the archive directory and some files in it
445    for (HRegion region : regions) {
446      Path archiveDir =
447        HFileArchiveTestingUtil.getRegionArchiveDir(UTIL.getConfiguration(), region);
448      assertTrue(fs.exists(archiveDir));
449
450      // check to make sure the store directory was copied
451      FileStatus[] stores =
452        fs.listStatus(archiveDir, p -> !p.getName().contains(HConstants.RECOVERED_EDITS_DIR));
453      assertTrue(stores.length == 1);
454
455      // make sure we archived the store files
456      FileStatus[] storeFiles = fs.listStatus(stores[0].getPath());
457      assertTrue(storeFiles.length > 0);
458    }
459
460    // then ensure the region's directories aren't present
461    for (Path regionDir : regionDirList) {
462      assertFalse(fs.exists(regionDir));
463    }
464
465    UTIL.deleteTable(tableName);
466  }
467
468  @Test(expected = IOException.class)
469  public void testArchiveRegionsWhenPermissionDenied() throws Exception {
470    final TableName tableName = TableName.valueOf(name.getMethodName());
471    List<HRegion> regions = initTableForArchivingRegions(tableName);
472
473    // now attempt to depose the regions
474    Path rootDir = CommonFSUtils.getRootDir(UTIL.getConfiguration());
475    Path tableDir = CommonFSUtils.getTableDir(rootDir, regions.get(0).getRegionInfo().getTable());
476    List<Path> regionDirList = regions.stream()
477      .map(region -> FSUtils.getRegionDirFromTableDir(tableDir, region.getRegionInfo()))
478      .collect(Collectors.toList());
479
480    // To create a permission denied error, we do archive regions as a non-current user
481    UserGroupInformation ugi =
482      UserGroupInformation.createUserForTesting("foo1234", new String[] { "group1" });
483
484    try {
485      ugi.doAs((PrivilegedExceptionAction<Void>) () -> {
486        FileSystem fs = UTIL.getTestFileSystem();
487        HFileArchiver.archiveRegions(UTIL.getConfiguration(), fs, rootDir, tableDir, regionDirList);
488        return null;
489      });
490    } catch (IOException e) {
491      assertTrue(e.getCause().getMessage().contains("Permission denied"));
492      throw e;
493    } finally {
494      UTIL.deleteTable(tableName);
495    }
496  }
497
498  @Test
499  public void testArchiveOnTableDelete() throws Exception {
500    final TableName tableName = TableName.valueOf(name.getMethodName());
501    UTIL.createTable(tableName, TEST_FAM);
502
503    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
504    // make sure we only have 1 region serving this table
505    assertEquals(1, servingRegions.size());
506    HRegion region = servingRegions.get(0);
507
508    // get the parent RS and monitor
509    HRegionServer hrs = UTIL.getRSForFirstRegionInTable(tableName);
510    FileSystem fs = hrs.getFileSystem();
511
512    // put some data on the region
513    LOG.debug("-------Loading table");
514    UTIL.loadRegion(region, TEST_FAM);
515
516    // get the hfiles in the region
517    List<HRegion> regions = hrs.getRegions(tableName);
518    assertEquals("More that 1 region for test table.", 1, regions.size());
519
520    region = regions.get(0);
521    // wait for all the compactions to complete
522    region.waitForFlushesAndCompactions();
523
524    // disable table to prevent new updates
525    UTIL.getAdmin().disableTable(tableName);
526    LOG.debug("Disabled table");
527
528    // remove all the files from the archive to get a fair comparison
529    clearArchiveDirectory();
530
531    // then get the current store files
532    byte[][] columns = region.getTableDescriptor().getColumnFamilyNames().toArray(new byte[0][]);
533    List<String> storeFiles = region.getStoreFileList(columns);
534
535    // then delete the table so the hfiles get archived
536    UTIL.deleteTable(tableName);
537    LOG.debug("Deleted table");
538
539    assertArchiveFiles(fs, storeFiles, 30000);
540  }
541
542  private void assertArchiveFiles(FileSystem fs, List<String> storeFiles, long timeout)
543    throws IOException {
544    long end = EnvironmentEdgeManager.currentTime() + timeout;
545    Path archiveDir = HFileArchiveUtil.getArchivePath(UTIL.getConfiguration());
546    List<String> archivedFiles = new ArrayList<>();
547
548    // We have to ensure that the DeleteTableHandler is finished. HBaseAdmin.deleteXXX()
549    // can return before all files
550    // are archived. We should fix HBASE-5487 and fix synchronous operations from admin.
551    while (EnvironmentEdgeManager.currentTime() < end) {
552      archivedFiles = getAllFileNames(fs, archiveDir);
553      if (archivedFiles.size() >= storeFiles.size()) {
554        break;
555      }
556    }
557
558    Collections.sort(storeFiles);
559    Collections.sort(archivedFiles);
560
561    LOG.debug("Store files:");
562    for (int i = 0; i < storeFiles.size(); i++) {
563      LOG.debug(i + " - " + storeFiles.get(i));
564    }
565    LOG.debug("Archive files:");
566    for (int i = 0; i < archivedFiles.size(); i++) {
567      LOG.debug(i + " - " + archivedFiles.get(i));
568    }
569
570    assertTrue("Archived files are missing some of the store files!",
571      archivedFiles.containsAll(storeFiles));
572  }
573
574  /**
575   * Test that the store files are archived when a column family is removed.
576   * @throws java.io.IOException            if there's a problem creating a table.
577   * @throws java.lang.InterruptedException problem getting a RegionServer.
578   */
579  @Test
580  public void testArchiveOnTableFamilyDelete() throws IOException, InterruptedException {
581    final TableName tableName = TableName.valueOf(name.getMethodName());
582    UTIL.createTable(tableName, new byte[][] { TEST_FAM, Bytes.toBytes("fam2") });
583
584    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
585    // make sure we only have 1 region serving this table
586    assertEquals(1, servingRegions.size());
587    HRegion region = servingRegions.get(0);
588
589    // get the parent RS and monitor
590    HRegionServer hrs = UTIL.getRSForFirstRegionInTable(tableName);
591    FileSystem fs = hrs.getFileSystem();
592
593    // put some data on the region
594    LOG.debug("-------Loading table");
595    UTIL.loadRegion(region, TEST_FAM);
596
597    // get the hfiles in the region
598    List<HRegion> regions = hrs.getRegions(tableName);
599    assertEquals("More that 1 region for test table.", 1, regions.size());
600
601    region = regions.get(0);
602    // wait for all the compactions to complete
603    region.waitForFlushesAndCompactions();
604
605    // disable table to prevent new updates
606    UTIL.getAdmin().disableTable(tableName);
607    LOG.debug("Disabled table");
608
609    // remove all the files from the archive to get a fair comparison
610    clearArchiveDirectory();
611
612    // then get the current store files
613    byte[][] columns = region.getTableDescriptor().getColumnFamilyNames().toArray(new byte[0][]);
614    List<String> storeFiles = region.getStoreFileList(columns);
615
616    // then delete the table so the hfiles get archived
617    UTIL.getAdmin().deleteColumnFamily(tableName, TEST_FAM);
618
619    assertArchiveFiles(fs, storeFiles, 30000);
620
621    UTIL.deleteTable(tableName);
622  }
623
624  /**
625   * Test HFileArchiver.resolveAndArchive() race condition HBASE-7643
626   */
627  @Test
628  public void testCleaningRace() throws Exception {
629    final long TEST_TIME = 20 * 1000;
630    final ChoreService choreService = new ChoreService("TEST_SERVER_NAME");
631
632    Configuration conf = UTIL.getMiniHBaseCluster().getMaster().getConfiguration();
633    Path rootDir = UTIL.getDataTestDirOnTestFS("testCleaningRace");
634    FileSystem fs = UTIL.getTestFileSystem();
635
636    Path archiveDir = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY);
637    Path regionDir = new Path(
638      CommonFSUtils.getTableDir(new Path("./"), TableName.valueOf(name.getMethodName())), "abcdef");
639    Path familyDir = new Path(regionDir, "cf");
640
641    Path sourceRegionDir = new Path(rootDir, regionDir);
642    fs.mkdirs(sourceRegionDir);
643
644    Stoppable stoppable = new StoppableImplementation();
645
646    // The cleaner should be looping without long pauses to reproduce the race condition.
647    HFileCleaner cleaner = getHFileCleaner(stoppable, conf, fs, archiveDir);
648    assertNotNull("cleaner should not be null", cleaner);
649    try {
650      choreService.scheduleChore(cleaner);
651      // Keep creating/archiving new files while the cleaner is running in the other thread
652      long startTime = EnvironmentEdgeManager.currentTime();
653      for (long fid = 0; (EnvironmentEdgeManager.currentTime() - startTime) < TEST_TIME; ++fid) {
654        Path file = new Path(familyDir, String.valueOf(fid));
655        Path sourceFile = new Path(rootDir, file);
656        Path archiveFile = new Path(archiveDir, file);
657
658        fs.createNewFile(sourceFile);
659
660        try {
661          // Try to archive the file
662          HFileArchiver.archiveRegion(fs, rootDir, sourceRegionDir.getParent(), sourceRegionDir);
663
664          // The archiver succeded, the file is no longer in the original location
665          // but it's in the archive location.
666          LOG.debug("hfile=" + fid + " should be in the archive");
667          assertTrue(fs.exists(archiveFile));
668          assertFalse(fs.exists(sourceFile));
669        } catch (IOException e) {
670          // The archiver is unable to archive the file. Probably HBASE-7643 race condition.
671          // in this case, the file should not be archived, and we should have the file
672          // in the original location.
673          LOG.debug("hfile=" + fid + " should be in the source location");
674          assertFalse(fs.exists(archiveFile));
675          assertTrue(fs.exists(sourceFile));
676
677          // Avoid to have this file in the next run
678          fs.delete(sourceFile, false);
679        }
680      }
681    } finally {
682      stoppable.stop("test end");
683      cleaner.cancel(true);
684      choreService.shutdown();
685      fs.delete(rootDir, true);
686    }
687  }
688
689  @Test
690  public void testArchiveRegionTableAndRegionDirsNull() throws IOException {
691    Path rootDir = UTIL.getDataTestDirOnTestFS("testCleaningRace");
692    FileSystem fileSystem = UTIL.getTestFileSystem();
693    // Try to archive the file but with null regionDir, can't delete sourceFile
694    assertFalse(HFileArchiver.archiveRegion(fileSystem, rootDir, null, null));
695  }
696
697  @Test
698  public void testArchiveRegionWithTableDirNull() throws IOException {
699    Path regionDir = new Path(
700      CommonFSUtils.getTableDir(new Path("./"), TableName.valueOf(name.getMethodName())), "xyzabc");
701    Path familyDir = new Path(regionDir, "rd");
702    Path rootDir = UTIL.getDataTestDirOnTestFS("testCleaningRace");
703    Path file = new Path(familyDir, "1");
704    Path sourceFile = new Path(rootDir, file);
705    FileSystem fileSystem = UTIL.getTestFileSystem();
706    fileSystem.createNewFile(sourceFile);
707    Path sourceRegionDir = new Path(rootDir, regionDir);
708    fileSystem.mkdirs(sourceRegionDir);
709    // Try to archive the file
710    assertFalse(HFileArchiver.archiveRegion(fileSystem, rootDir, null, sourceRegionDir));
711    assertFalse(fileSystem.exists(sourceRegionDir));
712  }
713
714  @Test
715  public void testArchiveRegionWithRegionDirNull() throws IOException {
716    Path regionDir =
717      new Path(CommonFSUtils.getTableDir(new Path("./"), TableName.valueOf(name.getMethodName())),
718        "elgn4nf");
719    Path familyDir = new Path(regionDir, "rdar");
720    Path rootDir = UTIL.getDataTestDirOnTestFS("testCleaningRace");
721    Path file = new Path(familyDir, "2");
722    Path sourceFile = new Path(rootDir, file);
723    FileSystem fileSystem = UTIL.getTestFileSystem();
724    fileSystem.createNewFile(sourceFile);
725    Path sourceRegionDir = new Path(rootDir, regionDir);
726    fileSystem.mkdirs(sourceRegionDir);
727    // Try to archive the file but with null regionDir, can't delete sourceFile
728    assertFalse(
729      HFileArchiver.archiveRegion(fileSystem, rootDir, sourceRegionDir.getParent(), null));
730    assertTrue(fileSystem.exists(sourceRegionDir));
731    fileSystem.delete(sourceRegionDir, true);
732  }
733
734  // Avoid passing a null master to CleanerChore, see HBASE-21175
735  private HFileCleaner getHFileCleaner(Stoppable stoppable, Configuration conf, FileSystem fs,
736    Path archiveDir) throws IOException {
737    Map<String, Object> params = new HashMap<>();
738    params.put(HMaster.MASTER, UTIL.getMiniHBaseCluster().getMaster());
739    HFileCleaner cleaner = new HFileCleaner(1, stoppable, conf, fs, archiveDir, POOL);
740    return Objects.requireNonNull(cleaner);
741  }
742
743  private void clearArchiveDirectory() throws IOException {
744    UTIL.getTestFileSystem()
745      .delete(new Path(UTIL.getDefaultRootDirPath(), HConstants.HFILE_ARCHIVE_DIRECTORY), true);
746  }
747
748  /**
749   * Get the names of all the files below the given directory
750   * @param fs         the file system to inspect
751   * @param archiveDir the directory in which to look
752   * @return a list of all files in the directory and sub-directories
753   * @throws java.io.IOException throws IOException in case FS is unavailable
754   */
755  private List<String> getAllFileNames(final FileSystem fs, Path archiveDir) throws IOException {
756    FileStatus[] files = CommonFSUtils.listStatus(fs, archiveDir, new PathFilter() {
757      @Override
758      public boolean accept(Path p) {
759        if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) {
760          return false;
761        }
762        return true;
763      }
764    });
765    return recurseOnFiles(fs, files, new ArrayList<>());
766  }
767
768  /** Recursively lookup all the file names under the file[] array **/
769  private List<String> recurseOnFiles(FileSystem fs, FileStatus[] files, List<String> fileNames)
770    throws IOException {
771    if (files == null || files.length == 0) {
772      return fileNames;
773    }
774
775    for (FileStatus file : files) {
776      if (file.isDirectory()) {
777        recurseOnFiles(fs, CommonFSUtils.listStatus(fs, file.getPath(), null), fileNames);
778      } else {
779        fileNames.add(file.getPath().getName());
780      }
781    }
782    return fileNames;
783  }
784}