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