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.master.cleaner;
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.List;
026import java.util.Random;
027import java.util.concurrent.ThreadLocalRandom;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FSDataOutputStream;
030import org.apache.hadoop.fs.FileStatus;
031import org.apache.hadoop.fs.FileSystem;
032import org.apache.hadoop.fs.Path;
033import org.apache.hadoop.hbase.HBaseClassTestRule;
034import org.apache.hadoop.hbase.HBaseTestingUtil;
035import org.apache.hadoop.hbase.HConstants;
036import org.apache.hadoop.hbase.Server;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.client.RegionInfoBuilder;
039import org.apache.hadoop.hbase.mob.ManualMobMaintHFileCleaner;
040import org.apache.hadoop.hbase.mob.MobUtils;
041import org.apache.hadoop.hbase.testclassification.MasterTests;
042import org.apache.hadoop.hbase.testclassification.MediumTests;
043import org.apache.hadoop.hbase.util.EnvironmentEdge;
044import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
045import org.apache.hadoop.hbase.util.HFileArchiveUtil;
046import org.apache.hadoop.hbase.util.MockServer;
047import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
048import org.junit.AfterClass;
049import org.junit.Assert;
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
057@Category({ MasterTests.class, MediumTests.class })
058public class TestHFileCleaner {
059
060  @ClassRule
061  public static final HBaseClassTestRule CLASS_RULE =
062    HBaseClassTestRule.forClass(TestHFileCleaner.class);
063
064  private static final Logger LOG = LoggerFactory.getLogger(TestHFileCleaner.class);
065
066  private final static HBaseTestingUtil UTIL = new HBaseTestingUtil();
067
068  private static DirScanPool POOL;
069
070  private static String MOCK_ARCHIVED_HFILE_DIR =
071    HConstants.HFILE_ARCHIVE_DIRECTORY + "/namespace/table/region";
072
073  @BeforeClass
074  public static void setupCluster() throws Exception {
075    // have to use a minidfs cluster because the localfs doesn't modify file times correctly
076    UTIL.startMiniDFSCluster(1);
077    POOL = DirScanPool.getHFileCleanerScanPool(UTIL.getConfiguration());
078  }
079
080  @AfterClass
081  public static void shutdownCluster() throws IOException {
082    UTIL.shutdownMiniDFSCluster();
083    POOL.shutdownNow();
084  }
085
086  @Test
087  public void testTTLCleaner() throws IOException {
088    FileSystem fs = UTIL.getDFSCluster().getFileSystem();
089    Path root = UTIL.getDataTestDirOnTestFS();
090    Path file = new Path(root, "file");
091    fs.createNewFile(file);
092    long createTime = EnvironmentEdgeManager.currentTime();
093    assertTrue("Test file not created!", fs.exists(file));
094    TimeToLiveHFileCleaner cleaner = new TimeToLiveHFileCleaner();
095    // update the time info for the file, so the cleaner removes it
096    fs.setTimes(file, createTime - 100, -1);
097    Configuration conf = UTIL.getConfiguration();
098    conf.setLong(TimeToLiveHFileCleaner.TTL_CONF_KEY, 100);
099    cleaner.setConf(conf);
100    assertTrue("File not set deletable - check mod time:" + getFileStats(file, fs)
101      + " with create time:" + createTime, cleaner.isFileDeletable(fs.getFileStatus(file)));
102  }
103
104  @Test
105  public void testManualMobCleanerStopsMobRemoval() throws IOException {
106    FileSystem fs = UTIL.getDFSCluster().getFileSystem();
107    Path root = UTIL.getDataTestDirOnTestFS();
108    TableName table = TableName.valueOf("testManualMobCleanerStopsMobRemoval");
109    Path mob = HFileArchiveUtil.getRegionArchiveDir(root, table,
110      MobUtils.getMobRegionInfo(table).getEncodedName());
111    Path family = new Path(mob, "family");
112
113    Path file = new Path(family, "someHFileThatWouldBeAUUID");
114    fs.createNewFile(file);
115    assertTrue("Test file not created!", fs.exists(file));
116
117    ManualMobMaintHFileCleaner cleaner = new ManualMobMaintHFileCleaner();
118
119    assertFalse("Mob File shouldn't have been deletable. check path. '" + file + "'",
120      cleaner.isFileDeletable(fs.getFileStatus(file)));
121  }
122
123  @Test
124  public void testManualMobCleanerLetsNonMobGo() throws IOException {
125    FileSystem fs = UTIL.getDFSCluster().getFileSystem();
126    Path root = UTIL.getDataTestDirOnTestFS();
127    TableName table = TableName.valueOf("testManualMobCleanerLetsNonMobGo");
128    Path nonmob = HFileArchiveUtil.getRegionArchiveDir(root, table,
129      RegionInfoBuilder.newBuilder(table).build().getEncodedName());
130    Path family = new Path(nonmob, "family");
131
132    Path file = new Path(family, "someHFileThatWouldBeAUUID");
133    fs.createNewFile(file);
134    assertTrue("Test file not created!", fs.exists(file));
135
136    ManualMobMaintHFileCleaner cleaner = new ManualMobMaintHFileCleaner();
137
138    assertTrue("Non-Mob File should have been deletable. check path. '" + file + "'",
139      cleaner.isFileDeletable(fs.getFileStatus(file)));
140  }
141
142  /**
143   * @param file to check
144   * @return loggable information about the file
145   */
146  private String getFileStats(Path file, FileSystem fs) throws IOException {
147    FileStatus status = fs.getFileStatus(file);
148    return "File" + file + ", mtime:" + status.getModificationTime() + ", atime:"
149      + status.getAccessTime();
150  }
151
152  @Test
153  public void testHFileCleaning() throws Exception {
154    final EnvironmentEdge originalEdge = EnvironmentEdgeManager.getDelegate();
155    String prefix = "someHFileThatWouldBeAUUID";
156    Configuration conf = UTIL.getConfiguration();
157    // set TTL
158    long ttl = 2000;
159    conf.set(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS,
160      "org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner,"
161        + "org.apache.hadoop.hbase.mob.ManualMobMaintHFileCleaner");
162    conf.setLong(TimeToLiveHFileCleaner.TTL_CONF_KEY, ttl);
163    Server server = new DummyServer();
164    Path archivedHfileDir = new Path(UTIL.getDataTestDirOnTestFS(), MOCK_ARCHIVED_HFILE_DIR);
165    FileSystem fs = FileSystem.get(conf);
166    HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir, POOL);
167
168    // Create 2 invalid files, 1 "recent" file, 1 very new file and 30 old files
169    final long createTime = EnvironmentEdgeManager.currentTime();
170    fs.delete(archivedHfileDir, true);
171    fs.mkdirs(archivedHfileDir);
172    // Case 1: 1 invalid file, which should be deleted directly
173    fs.createNewFile(new Path(archivedHfileDir, "dfd-dfd"));
174    // Case 2: 1 "recent" file, not even deletable for the first log cleaner
175    // (TimeToLiveLogCleaner), so we are not going down the chain
176    LOG.debug("Now is: " + createTime);
177    for (int i = 1; i < 32; i++) {
178      // Case 3: old files which would be deletable for the first log cleaner
179      // (TimeToLiveHFileCleaner),
180      Path fileName = new Path(archivedHfileDir, (prefix + "." + (createTime + i)));
181      fs.createNewFile(fileName);
182      // set the creation time past ttl to ensure that it gets removed
183      fs.setTimes(fileName, createTime - ttl - 1, -1);
184      LOG.debug("Creating " + getFileStats(fileName, fs));
185    }
186
187    // Case 2: 1 newer file, not even deletable for the first log cleaner
188    // (TimeToLiveLogCleaner), so we are not going down the chain
189    Path saved = new Path(archivedHfileDir, prefix + ".00000000000");
190    fs.createNewFile(saved);
191    // set creation time within the ttl
192    fs.setTimes(saved, createTime - ttl / 2, -1);
193    LOG.debug("Creating " + getFileStats(saved, fs));
194    for (FileStatus stat : fs.listStatus(archivedHfileDir)) {
195      LOG.debug(stat.getPath().toString());
196    }
197
198    assertEquals(33, fs.listStatus(archivedHfileDir).length);
199
200    // set a custom edge manager to handle time checking
201    EnvironmentEdge setTime = new EnvironmentEdge() {
202      @Override
203      public long currentTime() {
204        return createTime;
205      }
206    };
207    EnvironmentEdgeManager.injectEdge(setTime);
208
209    // run the chore
210    cleaner.chore();
211
212    // ensure we only end up with the saved file
213    assertEquals(1, fs.listStatus(archivedHfileDir).length);
214
215    for (FileStatus file : fs.listStatus(archivedHfileDir)) {
216      LOG.debug("Kept hfiles: " + file.getPath().getName());
217    }
218
219    // reset the edge back to the original edge
220    EnvironmentEdgeManager.injectEdge(originalEdge);
221  }
222
223  @Test
224  public void testRemovesEmptyDirectories() throws Exception {
225    Configuration conf = UTIL.getConfiguration();
226    // no cleaner policies = delete all files
227    conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
228    Server server = new DummyServer();
229    Path archivedHfileDir =
230      new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
231
232    // setup the cleaner
233    FileSystem fs = UTIL.getDFSCluster().getFileSystem();
234    HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir, POOL);
235
236    // make all the directories for archiving files
237    Path table = new Path(archivedHfileDir, "table");
238    Path region = new Path(table, "regionsomthing");
239    Path family = new Path(region, "fam");
240    Path file = new Path(family, "file12345");
241    fs.mkdirs(family);
242    if (!fs.exists(family)) throw new RuntimeException("Couldn't create test family:" + family);
243    fs.create(file).close();
244    if (!fs.exists(file)) throw new RuntimeException("Test file didn't get created:" + file);
245
246    // run the chore to cleanup the files (and the directories above it)
247    cleaner.chore();
248
249    // make sure all the parent directories get removed
250    assertFalse("family directory not removed for empty directory", fs.exists(family));
251    assertFalse("region directory not removed for empty directory", fs.exists(region));
252    assertFalse("table directory not removed for empty directory", fs.exists(table));
253    assertTrue("archive directory", fs.exists(archivedHfileDir));
254  }
255
256  static class DummyServer extends MockServer {
257    @Override
258    public Configuration getConfiguration() {
259      return UTIL.getConfiguration();
260    }
261
262    @Override
263    public ZKWatcher getZooKeeper() {
264      try {
265        return new ZKWatcher(getConfiguration(), "dummy server", this);
266      } catch (IOException e) {
267        e.printStackTrace();
268      }
269      return null;
270    }
271  }
272
273  @Test
274  public void testThreadCleanup() throws Exception {
275    Configuration conf = UTIL.getConfiguration();
276    conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
277    Server server = new DummyServer();
278    Path archivedHfileDir =
279      new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
280
281    // setup the cleaner
282    FileSystem fs = UTIL.getDFSCluster().getFileSystem();
283    HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir, POOL);
284    // clean up archive directory
285    fs.delete(archivedHfileDir, true);
286    fs.mkdirs(archivedHfileDir);
287    // create some file to delete
288    fs.createNewFile(new Path(archivedHfileDir, "dfd-dfd"));
289    // launch the chore
290    cleaner.chore();
291    // call cleanup
292    cleaner.cleanup();
293    // wait awhile for thread to die
294    Thread.sleep(100);
295    for (Thread thread : cleaner.getCleanerThreads()) {
296      Assert.assertFalse(thread.isAlive());
297    }
298  }
299
300  @Test
301  public void testLargeSmallIsolation() throws Exception {
302    Configuration conf = UTIL.getConfiguration();
303    // no cleaner policies = delete all files
304    conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
305    conf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, 512 * 1024);
306    Server server = new DummyServer();
307    Path archivedHfileDir =
308      new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
309
310    // setup the cleaner
311    FileSystem fs = UTIL.getDFSCluster().getFileSystem();
312    HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir, POOL);
313    // clean up archive directory
314    fs.delete(archivedHfileDir, true);
315    fs.mkdirs(archivedHfileDir);
316    // necessary set up
317    final int LARGE_FILE_NUM = 5;
318    final int SMALL_FILE_NUM = 20;
319    createFilesForTesting(LARGE_FILE_NUM, SMALL_FILE_NUM, fs, archivedHfileDir);
320    // call cleanup
321    cleaner.chore();
322
323    Assert.assertEquals(LARGE_FILE_NUM, cleaner.getNumOfDeletedLargeFiles());
324    Assert.assertEquals(SMALL_FILE_NUM, cleaner.getNumOfDeletedSmallFiles());
325  }
326
327  @Test
328  public void testOnConfigurationChange() throws Exception {
329    // constants
330    final int ORIGINAL_THROTTLE_POINT = 512 * 1024;
331    final int ORIGINAL_QUEUE_INIT_SIZE = 512;
332    final int UPDATE_THROTTLE_POINT = 1024;// small enough to change large/small check
333    final int UPDATE_QUEUE_INIT_SIZE = 1024;
334    final int LARGE_FILE_NUM = 5;
335    final int SMALL_FILE_NUM = 20;
336    final int LARGE_THREAD_NUM = 2;
337    final int SMALL_THREAD_NUM = 4;
338    final long THREAD_TIMEOUT_MSEC = 30 * 1000L;
339    final long THREAD_CHECK_INTERVAL_MSEC = 500L;
340
341    Configuration conf = UTIL.getConfiguration();
342    // no cleaner policies = delete all files
343    conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
344    conf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, ORIGINAL_THROTTLE_POINT);
345    conf.setInt(HFileCleaner.LARGE_HFILE_QUEUE_INIT_SIZE, ORIGINAL_QUEUE_INIT_SIZE);
346    conf.setInt(HFileCleaner.SMALL_HFILE_QUEUE_INIT_SIZE, ORIGINAL_QUEUE_INIT_SIZE);
347    Server server = new DummyServer();
348    Path archivedHfileDir =
349      new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
350
351    // setup the cleaner
352    FileSystem fs = UTIL.getDFSCluster().getFileSystem();
353    final HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir, POOL);
354    Assert.assertEquals(ORIGINAL_THROTTLE_POINT, cleaner.getThrottlePoint());
355    Assert.assertEquals(ORIGINAL_QUEUE_INIT_SIZE, cleaner.getLargeQueueInitSize());
356    Assert.assertEquals(ORIGINAL_QUEUE_INIT_SIZE, cleaner.getSmallQueueInitSize());
357    Assert.assertEquals(HFileCleaner.DEFAULT_HFILE_DELETE_THREAD_TIMEOUT_MSEC,
358      cleaner.getCleanerThreadTimeoutMsec());
359    Assert.assertEquals(HFileCleaner.DEFAULT_HFILE_DELETE_THREAD_CHECK_INTERVAL_MSEC,
360      cleaner.getCleanerThreadCheckIntervalMsec());
361
362    // clean up archive directory and create files for testing
363    fs.delete(archivedHfileDir, true);
364    fs.mkdirs(archivedHfileDir);
365    createFilesForTesting(LARGE_FILE_NUM, SMALL_FILE_NUM, fs, archivedHfileDir);
366
367    // call cleaner, run as daemon to test the interrupt-at-middle case
368    Thread t = new Thread() {
369      @Override
370      public void run() {
371        cleaner.chore();
372      }
373    };
374    t.setDaemon(true);
375    t.start();
376    // wait until file clean started
377    while (cleaner.getNumOfDeletedSmallFiles() == 0) {
378      Thread.yield();
379    }
380
381    // trigger configuration change
382    Configuration newConf = new Configuration(conf);
383    newConf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, UPDATE_THROTTLE_POINT);
384    newConf.setInt(HFileCleaner.LARGE_HFILE_QUEUE_INIT_SIZE, UPDATE_QUEUE_INIT_SIZE);
385    newConf.setInt(HFileCleaner.SMALL_HFILE_QUEUE_INIT_SIZE, UPDATE_QUEUE_INIT_SIZE);
386    newConf.setInt(HFileCleaner.LARGE_HFILE_DELETE_THREAD_NUMBER, LARGE_THREAD_NUM);
387    newConf.setInt(HFileCleaner.SMALL_HFILE_DELETE_THREAD_NUMBER, SMALL_THREAD_NUM);
388    newConf.setLong(HFileCleaner.HFILE_DELETE_THREAD_TIMEOUT_MSEC, THREAD_TIMEOUT_MSEC);
389    newConf.setLong(HFileCleaner.HFILE_DELETE_THREAD_CHECK_INTERVAL_MSEC,
390      THREAD_CHECK_INTERVAL_MSEC);
391
392    LOG.debug("File deleted from large queue: " + cleaner.getNumOfDeletedLargeFiles()
393      + "; from small queue: " + cleaner.getNumOfDeletedSmallFiles());
394    cleaner.onConfigurationChange(newConf);
395
396    // check values after change
397    Assert.assertEquals(UPDATE_THROTTLE_POINT, cleaner.getThrottlePoint());
398    Assert.assertEquals(UPDATE_QUEUE_INIT_SIZE, cleaner.getLargeQueueInitSize());
399    Assert.assertEquals(UPDATE_QUEUE_INIT_SIZE, cleaner.getSmallQueueInitSize());
400    Assert.assertEquals(LARGE_THREAD_NUM + SMALL_THREAD_NUM, cleaner.getCleanerThreads().size());
401    Assert.assertEquals(THREAD_TIMEOUT_MSEC, cleaner.getCleanerThreadTimeoutMsec());
402    Assert.assertEquals(THREAD_CHECK_INTERVAL_MSEC, cleaner.getCleanerThreadCheckIntervalMsec());
403
404    // make sure no cost when onConfigurationChange called with no change
405    List<Thread> oldThreads = cleaner.getCleanerThreads();
406    cleaner.onConfigurationChange(newConf);
407    List<Thread> newThreads = cleaner.getCleanerThreads();
408    Assert.assertArrayEquals(oldThreads.toArray(), newThreads.toArray());
409
410    // wait until clean done and check
411    t.join();
412    LOG.debug("File deleted from large queue: " + cleaner.getNumOfDeletedLargeFiles()
413      + "; from small queue: " + cleaner.getNumOfDeletedSmallFiles());
414    Assert.assertTrue(
415      "Should delete more than " + LARGE_FILE_NUM + " files from large queue but actually "
416        + cleaner.getNumOfDeletedLargeFiles(),
417      cleaner.getNumOfDeletedLargeFiles() > LARGE_FILE_NUM);
418    Assert.assertTrue(
419      "Should delete less than " + SMALL_FILE_NUM + " files from small queue but actually "
420        + cleaner.getNumOfDeletedSmallFiles(),
421      cleaner.getNumOfDeletedSmallFiles() < SMALL_FILE_NUM);
422  }
423
424  private void createFilesForTesting(int largeFileNum, int smallFileNum, FileSystem fs,
425    Path archivedHfileDir) throws IOException {
426    final Random rand = ThreadLocalRandom.current();
427    final byte[] large = new byte[1024 * 1024];
428    for (int i = 0; i < large.length; i++) {
429      large[i] = (byte) rand.nextInt(128);
430    }
431    final byte[] small = new byte[1024];
432    for (int i = 0; i < small.length; i++) {
433      small[i] = (byte) rand.nextInt(128);
434    }
435    // create large and small files
436    for (int i = 1; i <= largeFileNum; i++) {
437      FSDataOutputStream out = fs.create(new Path(archivedHfileDir, "large-file-" + i));
438      out.write(large);
439      out.close();
440    }
441    for (int i = 1; i <= smallFileNum; i++) {
442      FSDataOutputStream out = fs.create(new Path(archivedHfileDir, "small-file-" + i));
443      out.write(small);
444      out.close();
445    }
446  }
447}