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}