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.assertTrue;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FileStatus;
030import org.apache.hadoop.fs.FileSystem;
031import org.apache.hadoop.fs.Path;
032import org.apache.hadoop.fs.PathFilter;
033import org.apache.hadoop.hbase.ChoreService;
034import org.apache.hadoop.hbase.HBaseClassTestRule;
035import org.apache.hadoop.hbase.HBaseTestingUtility;
036import org.apache.hadoop.hbase.HConstants;
037import org.apache.hadoop.hbase.Stoppable;
038import org.apache.hadoop.hbase.TableName;
039import org.apache.hadoop.hbase.client.Admin;
040import org.apache.hadoop.hbase.master.cleaner.HFileCleaner;
041import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy;
042import org.apache.hadoop.hbase.regionserver.HRegion;
043import org.apache.hadoop.hbase.regionserver.HRegionServer;
044import org.apache.hadoop.hbase.testclassification.MediumTests;
045import org.apache.hadoop.hbase.testclassification.MiscTests;
046import org.apache.hadoop.hbase.util.Bytes;
047import org.apache.hadoop.hbase.util.FSUtils;
048import org.apache.hadoop.hbase.util.HFileArchiveTestingUtil;
049import org.apache.hadoop.hbase.util.HFileArchiveUtil;
050import org.apache.hadoop.hbase.util.StoppableImplementation;
051import org.junit.After;
052import org.junit.AfterClass;
053import org.junit.Assert;
054import org.junit.BeforeClass;
055import org.junit.ClassRule;
056import org.junit.Rule;
057import org.junit.Test;
058import org.junit.experimental.categories.Category;
059import org.junit.rules.TestName;
060import org.slf4j.Logger;
061import org.slf4j.LoggerFactory;
062
063/**
064 * Test that the {@link HFileArchiver} correctly removes all the parts of a region when cleaning up
065 * a region
066 */
067@Category({MediumTests.class, MiscTests.class})
068public class TestHFileArchiving {
069
070  @ClassRule
071  public static final HBaseClassTestRule CLASS_RULE =
072      HBaseClassTestRule.forClass(TestHFileArchiving.class);
073
074  private static final Logger LOG = LoggerFactory.getLogger(TestHFileArchiving.class);
075  private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
076  private static final byte[] TEST_FAM = Bytes.toBytes("fam");
077
078  @Rule
079  public TestName name = new TestName();
080
081  /**
082   * Setup the config for the cluster
083   */
084  @BeforeClass
085  public static void setupCluster() throws Exception {
086    setupConf(UTIL.getConfiguration());
087    UTIL.startMiniCluster();
088
089    // We don't want the cleaner to remove files. The tests do that.
090    UTIL.getMiniHBaseCluster().getMaster().getHFileCleaner().cancel(true);
091  }
092
093  private static void setupConf(Configuration conf) {
094    // disable the ui
095    conf.setInt("hbase.regionsever.info.port", -1);
096    // drop the memstore size so we get flushes
097    conf.setInt("hbase.hregion.memstore.flush.size", 25000);
098    // disable major compactions
099    conf.setInt(HConstants.MAJOR_COMPACTION_PERIOD, 0);
100
101    // prevent aggressive region split
102    conf.set(HConstants.HBASE_REGION_SPLIT_POLICY_KEY,
103      ConstantSizeRegionSplitPolicy.class.getName());
104  }
105
106  @After
107  public void tearDown() throws Exception {
108    // cleanup the archive directory
109    try {
110      clearArchiveDirectory();
111    } catch (IOException e) {
112      Assert.fail("Failure to delete archive directory:" + e.getMessage());
113    }
114  }
115
116  @AfterClass
117  public static void cleanupTest() throws Exception {
118    try {
119      UTIL.shutdownMiniCluster();
120    } catch (Exception e) {
121      // NOOP;
122    }
123  }
124
125  @Test
126  public void testRemovesRegionDirOnArchive() throws Exception {
127    final TableName tableName = TableName.valueOf(name.getMethodName());
128    UTIL.createTable(tableName, TEST_FAM);
129
130    final Admin admin = UTIL.getAdmin();
131
132    // get the current store files for the region
133    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
134    // make sure we only have 1 region serving this table
135    assertEquals(1, servingRegions.size());
136    HRegion region = servingRegions.get(0);
137
138    // and load the table
139    UTIL.loadRegion(region, TEST_FAM);
140
141    // shutdown the table so we can manipulate the files
142    admin.disableTable(tableName);
143
144    FileSystem fs = UTIL.getTestFileSystem();
145
146    // now attempt to depose the region
147    Path rootDir = region.getRegionFileSystem().getTableDir().getParent();
148    Path regionDir = FSUtils.getRegionDirFromRootDir(rootDir, region.getRegionInfo());
149
150    HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo());
151
152    // check for the existence of the archive directory and some files in it
153    Path archiveDir = HFileArchiveTestingUtil.getRegionArchiveDir(UTIL.getConfiguration(), region);
154    assertTrue(fs.exists(archiveDir));
155
156    // check to make sure the store directory was copied
157    // check to make sure the store directory was copied
158    FileStatus[] stores = fs.listStatus(archiveDir, new PathFilter() {
159      @Override
160      public boolean accept(Path p) {
161        if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) {
162          return false;
163        }
164        return true;
165      }
166    });
167    assertTrue(stores.length == 1);
168
169    // make sure we archived the store files
170    FileStatus[] storeFiles = fs.listStatus(stores[0].getPath());
171    assertTrue(storeFiles.length > 0);
172
173    // then ensure the region's directory isn't present
174    assertFalse(fs.exists(regionDir));
175
176    UTIL.deleteTable(tableName);
177  }
178
179  /**
180   * Test that the region directory is removed when we archive a region without store files, but
181   * still has hidden files.
182   * @throws Exception
183   */
184  @Test
185  public void testDeleteRegionWithNoStoreFiles() throws Exception {
186    final TableName tableName = TableName.valueOf(name.getMethodName());
187    UTIL.createTable(tableName, TEST_FAM);
188
189    // get the current store files for the region
190    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
191    // make sure we only have 1 region serving this table
192    assertEquals(1, servingRegions.size());
193    HRegion region = servingRegions.get(0);
194
195    FileSystem fs = region.getRegionFileSystem().getFileSystem();
196
197    // make sure there are some files in the regiondir
198    Path rootDir = FSUtils.getRootDir(fs.getConf());
199    Path regionDir = FSUtils.getRegionDirFromRootDir(rootDir, region.getRegionInfo());
200    FileStatus[] regionFiles = FSUtils.listStatus(fs, regionDir, null);
201    Assert.assertNotNull("No files in the region directory", regionFiles);
202    if (LOG.isDebugEnabled()) {
203      List<Path> files = new ArrayList<>();
204      for (FileStatus file : regionFiles) {
205        files.add(file.getPath());
206      }
207      LOG.debug("Current files:" + files);
208    }
209    // delete the visible folders so we just have hidden files/folders
210    final PathFilter dirFilter = new FSUtils.DirFilter(fs);
211    PathFilter nonHidden = new PathFilter() {
212      @Override
213      public boolean accept(Path file) {
214        return dirFilter.accept(file) && !file.getName().toString().startsWith(".");
215      }
216    };
217    FileStatus[] storeDirs = FSUtils.listStatus(fs, regionDir, nonHidden);
218    for (FileStatus store : storeDirs) {
219      LOG.debug("Deleting store for test");
220      fs.delete(store.getPath(), true);
221    }
222
223    // then archive the region
224    HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo());
225
226    // and check to make sure the region directoy got deleted
227    assertFalse("Region directory (" + regionDir + "), still exists.", fs.exists(regionDir));
228
229    UTIL.deleteTable(tableName);
230  }
231
232  @Test
233  public void testArchiveOnTableDelete() throws Exception {
234    final TableName tableName = TableName.valueOf(name.getMethodName());
235    UTIL.createTable(tableName, TEST_FAM);
236
237    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
238    // make sure we only have 1 region serving this table
239    assertEquals(1, servingRegions.size());
240    HRegion region = servingRegions.get(0);
241
242    // get the parent RS and monitor
243    HRegionServer hrs = UTIL.getRSForFirstRegionInTable(tableName);
244    FileSystem fs = hrs.getFileSystem();
245
246    // put some data on the region
247    LOG.debug("-------Loading table");
248    UTIL.loadRegion(region, TEST_FAM);
249
250    // get the hfiles in the region
251    List<HRegion> regions = hrs.getRegions(tableName);
252    assertEquals("More that 1 region for test table.", 1, regions.size());
253
254    region = regions.get(0);
255    // wait for all the compactions to complete
256    region.waitForFlushesAndCompactions();
257
258    // disable table to prevent new updates
259    UTIL.getAdmin().disableTable(tableName);
260    LOG.debug("Disabled table");
261
262    // remove all the files from the archive to get a fair comparison
263    clearArchiveDirectory();
264
265    // then get the current store files
266    byte[][]columns = region.getTableDescriptor().getColumnFamilyNames().toArray(new byte[0][]);
267    List<String> storeFiles = region.getStoreFileList(columns);
268
269    // then delete the table so the hfiles get archived
270    UTIL.deleteTable(tableName);
271    LOG.debug("Deleted table");
272
273    assertArchiveFiles(fs, storeFiles, 30000);
274  }
275
276  private void assertArchiveFiles(FileSystem fs, List<String> storeFiles, long timeout) throws IOException {
277    long end = System.currentTimeMillis() + timeout;
278    Path archiveDir = HFileArchiveUtil.getArchivePath(UTIL.getConfiguration());
279    List<String> archivedFiles = new ArrayList<>();
280
281    // We have to ensure that the DeleteTableHandler is finished. HBaseAdmin.deleteXXX() can return before all files
282    // are archived. We should fix HBASE-5487 and fix synchronous operations from admin.
283    while (System.currentTimeMillis() < end) {
284      archivedFiles = getAllFileNames(fs, archiveDir);
285      if (archivedFiles.size() >= storeFiles.size()) {
286        break;
287      }
288    }
289
290    Collections.sort(storeFiles);
291    Collections.sort(archivedFiles);
292
293    LOG.debug("Store files:");
294    for (int i = 0; i < storeFiles.size(); i++) {
295      LOG.debug(i + " - " + storeFiles.get(i));
296    }
297    LOG.debug("Archive files:");
298    for (int i = 0; i < archivedFiles.size(); i++) {
299      LOG.debug(i + " - " + archivedFiles.get(i));
300    }
301
302    assertTrue("Archived files are missing some of the store files!",
303      archivedFiles.containsAll(storeFiles));
304  }
305
306
307  /**
308   * Test that the store files are archived when a column family is removed.
309   * @throws Exception
310   */
311  @Test
312  public void testArchiveOnTableFamilyDelete() throws Exception {
313    final TableName tableName = TableName.valueOf(name.getMethodName());
314    UTIL.createTable(tableName, new byte[][] {TEST_FAM, Bytes.toBytes("fam2")});
315
316    List<HRegion> servingRegions = UTIL.getHBaseCluster().getRegions(tableName);
317    // make sure we only have 1 region serving this table
318    assertEquals(1, servingRegions.size());
319    HRegion region = servingRegions.get(0);
320
321    // get the parent RS and monitor
322    HRegionServer hrs = UTIL.getRSForFirstRegionInTable(tableName);
323    FileSystem fs = hrs.getFileSystem();
324
325    // put some data on the region
326    LOG.debug("-------Loading table");
327    UTIL.loadRegion(region, TEST_FAM);
328
329    // get the hfiles in the region
330    List<HRegion> regions = hrs.getRegions(tableName);
331    assertEquals("More that 1 region for test table.", 1, regions.size());
332
333    region = regions.get(0);
334    // wait for all the compactions to complete
335    region.waitForFlushesAndCompactions();
336
337    // disable table to prevent new updates
338    UTIL.getAdmin().disableTable(tableName);
339    LOG.debug("Disabled table");
340
341    // remove all the files from the archive to get a fair comparison
342    clearArchiveDirectory();
343
344    // then get the current store files
345    byte[][]columns = region.getTableDescriptor().getColumnFamilyNames().toArray(new byte[0][]);
346    List<String> storeFiles = region.getStoreFileList(columns);
347
348    // then delete the table so the hfiles get archived
349    UTIL.getAdmin().deleteColumnFamily(tableName, TEST_FAM);
350
351    assertArchiveFiles(fs, storeFiles, 30000);
352
353    UTIL.deleteTable(tableName);
354  }
355
356  /**
357   * Test HFileArchiver.resolveAndArchive() race condition HBASE-7643
358   */
359  @Test
360  public void testCleaningRace() throws Exception {
361    final long TEST_TIME = 20 * 1000;
362    final ChoreService choreService = new ChoreService("TEST_SERVER_NAME");
363
364    Configuration conf = UTIL.getMiniHBaseCluster().getMaster().getConfiguration();
365    Path rootDir = UTIL.getDataTestDirOnTestFS("testCleaningRace");
366    FileSystem fs = UTIL.getTestFileSystem();
367
368    Path archiveDir = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY);
369    Path regionDir = new Path(FSUtils.getTableDir(new Path("./"),
370        TableName.valueOf(name.getMethodName())), "abcdef");
371    Path familyDir = new Path(regionDir, "cf");
372
373    Path sourceRegionDir = new Path(rootDir, regionDir);
374    fs.mkdirs(sourceRegionDir);
375
376    Stoppable stoppable = new StoppableImplementation();
377
378    // The cleaner should be looping without long pauses to reproduce the race condition.
379    HFileCleaner cleaner = new HFileCleaner(1, stoppable, conf, fs, archiveDir);
380    try {
381      choreService.scheduleChore(cleaner);
382
383      // Keep creating/archiving new files while the cleaner is running in the other thread
384      long startTime = System.currentTimeMillis();
385      for (long fid = 0; (System.currentTimeMillis() - startTime) < TEST_TIME; ++fid) {
386        Path file = new Path(familyDir,  String.valueOf(fid));
387        Path sourceFile = new Path(rootDir, file);
388        Path archiveFile = new Path(archiveDir, file);
389
390        fs.createNewFile(sourceFile);
391
392        try {
393          // Try to archive the file
394          HFileArchiver.archiveRegion(fs, rootDir,
395              sourceRegionDir.getParent(), sourceRegionDir);
396
397          // The archiver succeded, the file is no longer in the original location
398          // but it's in the archive location.
399          LOG.debug("hfile=" + fid + " should be in the archive");
400          assertTrue(fs.exists(archiveFile));
401          assertFalse(fs.exists(sourceFile));
402        } catch (IOException e) {
403          // The archiver is unable to archive the file. Probably HBASE-7643 race condition.
404          // in this case, the file should not be archived, and we should have the file
405          // in the original location.
406          LOG.debug("hfile=" + fid + " should be in the source location");
407          assertFalse(fs.exists(archiveFile));
408          assertTrue(fs.exists(sourceFile));
409
410          // Avoid to have this file in the next run
411          fs.delete(sourceFile, false);
412        }
413      }
414    } finally {
415      stoppable.stop("test end");
416      cleaner.cancel(true);
417      choreService.shutdown();
418      fs.delete(rootDir, true);
419    }
420  }
421
422  private void clearArchiveDirectory() throws IOException {
423    UTIL.getTestFileSystem().delete(
424      new Path(UTIL.getDefaultRootDirPath(), HConstants.HFILE_ARCHIVE_DIRECTORY), true);
425  }
426
427  /**
428   * Get the names of all the files below the given directory
429   * @param fs
430   * @param archiveDir
431   * @return
432   * @throws IOException
433   */
434  private List<String> getAllFileNames(final FileSystem fs, Path archiveDir) throws IOException {
435    FileStatus[] files = FSUtils.listStatus(fs, archiveDir, new PathFilter() {
436      @Override
437      public boolean accept(Path p) {
438        if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) {
439          return false;
440        }
441        return true;
442      }
443    });
444    return recurseOnFiles(fs, files, new ArrayList<>());
445  }
446
447  /** Recursively lookup all the file names under the file[] array **/
448  private List<String> recurseOnFiles(FileSystem fs, FileStatus[] files, List<String> fileNames)
449      throws IOException {
450    if (files == null || files.length == 0) return fileNames;
451
452    for (FileStatus file : files) {
453      if (file.isDirectory()) {
454        recurseOnFiles(fs, FSUtils.listStatus(fs, file.getPath(), null), fileNames);
455      } else fileNames.add(file.getPath().getName());
456    }
457    return fileNames;
458  }
459}