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}