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.concurrent.ThreadLocalRandom; 026import java.util.concurrent.atomic.AtomicBoolean; 027import org.apache.hadoop.conf.Configuration; 028import org.apache.hadoop.fs.FSDataOutputStream; 029import org.apache.hadoop.fs.FileStatus; 030import org.apache.hadoop.fs.FileSystem; 031import org.apache.hadoop.fs.FilterFileSystem; 032import org.apache.hadoop.fs.Path; 033import org.apache.hadoop.hbase.HBaseClassTestRule; 034import org.apache.hadoop.hbase.HBaseTestingUtility; 035import org.apache.hadoop.hbase.Stoppable; 036import org.apache.hadoop.hbase.testclassification.MasterTests; 037import org.apache.hadoop.hbase.testclassification.SmallTests; 038import org.apache.hadoop.hbase.util.Bytes; 039import org.apache.hadoop.hbase.util.CommonFSUtils; 040import org.apache.hadoop.hbase.util.StoppableImplementation; 041import org.junit.AfterClass; 042import org.junit.BeforeClass; 043import org.junit.ClassRule; 044import org.junit.Test; 045import org.junit.experimental.categories.Category; 046import org.mockito.Mockito; 047import org.mockito.invocation.InvocationOnMock; 048import org.mockito.stubbing.Answer; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052@Category({ MasterTests.class, SmallTests.class }) 053public class TestCleanerChore { 054 055 @ClassRule 056 public static final HBaseClassTestRule CLASS_RULE = 057 HBaseClassTestRule.forClass(TestCleanerChore.class); 058 059 private static final Logger LOG = LoggerFactory.getLogger(TestCleanerChore.class); 060 private static final HBaseTestingUtility UTIL = new HBaseTestingUtility(); 061 private static DirScanPool POOL; 062 063 @BeforeClass 064 public static void setup() { 065 POOL = DirScanPool.getHFileCleanerScanPool(UTIL.getConfiguration()); 066 } 067 068 @AfterClass 069 public static void cleanup() throws Exception { 070 // delete and recreate the test directory, ensuring a clean test dir between tests 071 UTIL.cleanupTestDir(); 072 POOL.shutdownNow(); 073 } 074 075 @Test 076 public void testSavesFilesOnRequest() throws Exception { 077 Stoppable stop = new StoppableImplementation(); 078 Configuration conf = UTIL.getConfiguration(); 079 Path testDir = UTIL.getDataTestDir(); 080 FileSystem fs = UTIL.getTestFileSystem(); 081 String confKey = "hbase.test.cleaner.delegates"; 082 conf.set(confKey, NeverDelete.class.getName()); 083 084 AllValidPaths chore = 085 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 086 087 // create the directory layout in the directory to clean 088 Path parent = new Path(testDir, "parent"); 089 Path file = new Path(parent, "someFile"); 090 fs.mkdirs(parent); 091 // touch a new file 092 fs.create(file).close(); 093 assertTrue("Test file didn't get created.", fs.exists(file)); 094 095 // run the chore 096 chore.chore(); 097 098 // verify all the files were preserved 099 assertTrue("File shouldn't have been deleted", fs.exists(file)); 100 assertTrue("directory shouldn't have been deleted", fs.exists(parent)); 101 } 102 103 @Test 104 public void retriesIOExceptionInStatus() throws Exception { 105 Stoppable stop = new StoppableImplementation(); 106 Configuration conf = UTIL.getConfiguration(); 107 Path testDir = UTIL.getDataTestDir(); 108 FileSystem fs = UTIL.getTestFileSystem(); 109 String confKey = "hbase.test.cleaner.delegates"; 110 111 Path child = new Path(testDir, "child"); 112 Path file = new Path(child, "file"); 113 fs.mkdirs(child); 114 fs.create(file).close(); 115 assertTrue("test file didn't get created.", fs.exists(file)); 116 final AtomicBoolean fails = new AtomicBoolean(true); 117 118 FilterFileSystem filtered = new FilterFileSystem(fs) { 119 public FileStatus[] listStatus(Path f) throws IOException { 120 if (fails.get()) { 121 throw new IOException("whomp whomp."); 122 } 123 return fs.listStatus(f); 124 } 125 }; 126 127 AllValidPaths chore = 128 new AllValidPaths("test-retry-ioe", stop, conf, filtered, testDir, confKey, POOL); 129 130 // trouble talking to the filesystem 131 Boolean result = chore.runCleaner(); 132 133 // verify that it couldn't clean the files. 134 assertTrue("test rig failed to inject failure.", fs.exists(file)); 135 assertTrue("test rig failed to inject failure.", fs.exists(child)); 136 // and verify that it accurately reported the failure. 137 assertFalse("chore should report that it failed.", result); 138 139 // filesystem is back 140 fails.set(false); 141 result = chore.runCleaner(); 142 143 // verify everything is gone. 144 assertFalse("file should have been destroyed.", fs.exists(file)); 145 assertFalse("directory should have been destroyed.", fs.exists(child)); 146 // and verify that it accurately reported success. 147 assertTrue("chore should claim it succeeded.", result); 148 } 149 150 @Test 151 public void testDeletesEmptyDirectories() throws Exception { 152 Stoppable stop = new StoppableImplementation(); 153 Configuration conf = UTIL.getConfiguration(); 154 Path testDir = UTIL.getDataTestDir(); 155 FileSystem fs = UTIL.getTestFileSystem(); 156 String confKey = "hbase.test.cleaner.delegates"; 157 conf.set(confKey, AlwaysDelete.class.getName()); 158 159 AllValidPaths chore = 160 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 161 162 // create the directory layout in the directory to clean 163 Path parent = new Path(testDir, "parent"); 164 Path child = new Path(parent, "child"); 165 Path emptyChild = new Path(parent, "emptyChild"); 166 Path file = new Path(child, "someFile"); 167 fs.mkdirs(child); 168 fs.mkdirs(emptyChild); 169 // touch a new file 170 fs.create(file).close(); 171 // also create a file in the top level directory 172 Path topFile = new Path(testDir, "topFile"); 173 fs.create(topFile).close(); 174 assertTrue("Test file didn't get created.", fs.exists(file)); 175 assertTrue("Test file didn't get created.", fs.exists(topFile)); 176 177 // run the chore 178 chore.chore(); 179 180 // verify all the files got deleted 181 assertFalse("File didn't get deleted", fs.exists(topFile)); 182 assertFalse("File didn't get deleted", fs.exists(file)); 183 assertFalse("Empty directory didn't get deleted", fs.exists(child)); 184 assertFalse("Empty directory didn't get deleted", fs.exists(parent)); 185 } 186 187 /** 188 * Test to make sure that we don't attempt to ask the delegate whether or not we should preserve a 189 * directory. 190 * @throws Exception on failure 191 */ 192 @Test 193 public void testDoesNotCheckDirectories() throws Exception { 194 Stoppable stop = new StoppableImplementation(); 195 Configuration conf = UTIL.getConfiguration(); 196 Path testDir = UTIL.getDataTestDir(); 197 FileSystem fs = UTIL.getTestFileSystem(); 198 String confKey = "hbase.test.cleaner.delegates"; 199 conf.set(confKey, AlwaysDelete.class.getName()); 200 201 AllValidPaths chore = 202 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 203 // spy on the delegate to ensure that we don't check for directories 204 AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0); 205 AlwaysDelete spy = Mockito.spy(delegate); 206 chore.cleanersChain.set(0, spy); 207 208 // create the directory layout in the directory to clean 209 Path parent = new Path(testDir, "parent"); 210 Path file = new Path(parent, "someFile"); 211 fs.mkdirs(parent); 212 assertTrue("Test parent didn't get created.", fs.exists(parent)); 213 // touch a new file 214 fs.create(file).close(); 215 assertTrue("Test file didn't get created.", fs.exists(file)); 216 217 FileStatus fStat = fs.getFileStatus(parent); 218 chore.chore(); 219 // make sure we never checked the directory 220 Mockito.verify(spy, Mockito.never()).isFileDeletable(fStat); 221 Mockito.reset(spy); 222 } 223 224 @Test 225 public void testStoppedCleanerDoesNotDeleteFiles() throws Exception { 226 Stoppable stop = new StoppableImplementation(); 227 Configuration conf = UTIL.getConfiguration(); 228 Path testDir = UTIL.getDataTestDir(); 229 FileSystem fs = UTIL.getTestFileSystem(); 230 String confKey = "hbase.test.cleaner.delegates"; 231 conf.set(confKey, AlwaysDelete.class.getName()); 232 233 AllValidPaths chore = 234 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 235 236 // also create a file in the top level directory 237 Path topFile = new Path(testDir, "topFile"); 238 fs.create(topFile).close(); 239 assertTrue("Test file didn't get created.", fs.exists(topFile)); 240 241 // stop the chore 242 stop.stop("testing stop"); 243 244 // run the chore 245 chore.chore(); 246 247 // test that the file still exists 248 assertTrue("File got deleted while chore was stopped", fs.exists(topFile)); 249 } 250 251 /** 252 * While cleaning a directory, all the files in the directory may be deleted, but there may be 253 * another file added, in which case the directory shouldn't be deleted. 254 * @throws IOException on failure 255 */ 256 @Test 257 public void testCleanerDoesNotDeleteDirectoryWithLateAddedFiles() throws IOException { 258 Stoppable stop = new StoppableImplementation(); 259 Configuration conf = UTIL.getConfiguration(); 260 final Path testDir = UTIL.getDataTestDir(); 261 final FileSystem fs = UTIL.getTestFileSystem(); 262 String confKey = "hbase.test.cleaner.delegates"; 263 conf.set(confKey, AlwaysDelete.class.getName()); 264 265 AllValidPaths chore = 266 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 267 // spy on the delegate to ensure that we don't check for directories 268 AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0); 269 AlwaysDelete spy = Mockito.spy(delegate); 270 chore.cleanersChain.set(0, spy); 271 272 // create the directory layout in the directory to clean 273 final Path parent = new Path(testDir, "parent"); 274 Path file = new Path(parent, "someFile"); 275 fs.mkdirs(parent); 276 // touch a new file 277 fs.create(file).close(); 278 assertTrue("Test file didn't get created.", fs.exists(file)); 279 final Path addedFile = new Path(parent, "addedFile"); 280 281 // when we attempt to delete the original file, add another file in the same directory 282 Mockito.doAnswer(new Answer<Boolean>() { 283 @Override 284 public Boolean answer(InvocationOnMock invocation) throws Throwable { 285 fs.create(addedFile).close(); 286 CommonFSUtils.logFileSystemState(fs, testDir, LOG); 287 return (Boolean) invocation.callRealMethod(); 288 } 289 }).when(spy).isFileDeletable(Mockito.any()); 290 291 // run the chore 292 chore.chore(); 293 294 // make sure all the directories + added file exist, but the original file is deleted 295 assertTrue("Added file unexpectedly deleted", fs.exists(addedFile)); 296 assertTrue("Parent directory deleted unexpectedly", fs.exists(parent)); 297 assertFalse("Original file unexpectedly retained", fs.exists(file)); 298 Mockito.verify(spy, Mockito.times(1)).isFileDeletable(Mockito.any()); 299 Mockito.reset(spy); 300 } 301 302 /** 303 * The cleaner runs in a loop, where it first checks to see all the files under a directory can be 304 * deleted. If they all can, then we try to delete the directory. However, a file may be added 305 * that directory to after the original check. This ensures that we don't accidentally delete that 306 * directory on and don't get spurious IOExceptions. 307 * <p> 308 * This was from HBASE-7465. 309 * @throws Exception on failure 310 */ 311 @Test 312 public void testNoExceptionFromDirectoryWithRacyChildren() throws Exception { 313 UTIL.cleanupTestDir(); 314 Stoppable stop = new StoppableImplementation(); 315 // need to use a localutil to not break the rest of the test that runs on the local FS, which 316 // gets hosed when we start to use a minicluster. 317 HBaseTestingUtility localUtil = new HBaseTestingUtility(); 318 Configuration conf = localUtil.getConfiguration(); 319 final Path testDir = UTIL.getDataTestDir(); 320 final FileSystem fs = UTIL.getTestFileSystem(); 321 LOG.debug("Writing test data to: " + testDir); 322 String confKey = "hbase.test.cleaner.delegates"; 323 conf.set(confKey, AlwaysDelete.class.getName()); 324 325 AllValidPaths chore = 326 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 327 // spy on the delegate to ensure that we don't check for directories 328 AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0); 329 AlwaysDelete spy = Mockito.spy(delegate); 330 chore.cleanersChain.set(0, spy); 331 332 // create the directory layout in the directory to clean 333 final Path parent = new Path(testDir, "parent"); 334 Path file = new Path(parent, "someFile"); 335 fs.mkdirs(parent); 336 // touch a new file 337 fs.create(file).close(); 338 assertTrue("Test file didn't get created.", fs.exists(file)); 339 final Path racyFile = new Path(parent, "addedFile"); 340 341 // when we attempt to delete the original file, add another file in the same directory 342 Mockito.doAnswer(new Answer<Boolean>() { 343 @Override 344 public Boolean answer(InvocationOnMock invocation) throws Throwable { 345 fs.create(racyFile).close(); 346 CommonFSUtils.logFileSystemState(fs, testDir, LOG); 347 return (Boolean) invocation.callRealMethod(); 348 } 349 }).when(spy).isFileDeletable(Mockito.any()); 350 351 // run the chore 352 chore.chore(); 353 354 // make sure all the directories + added file exist, but the original file is deleted 355 assertTrue("Added file unexpectedly deleted", fs.exists(racyFile)); 356 assertTrue("Parent directory deleted unexpectedly", fs.exists(parent)); 357 assertFalse("Original file unexpectedly retained", fs.exists(file)); 358 Mockito.verify(spy, Mockito.times(1)).isFileDeletable(Mockito.any()); 359 } 360 361 @Test 362 public void testDeleteFileWithCleanerEnabled() throws Exception { 363 Stoppable stop = new StoppableImplementation(); 364 Configuration conf = UTIL.getConfiguration(); 365 Path testDir = UTIL.getDataTestDir(); 366 FileSystem fs = UTIL.getTestFileSystem(); 367 String confKey = "hbase.test.cleaner.delegates"; 368 conf.set(confKey, AlwaysDelete.class.getName()); 369 370 AllValidPaths chore = 371 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 372 373 // Enable cleaner 374 chore.setEnabled(true); 375 376 // create the directory layout in the directory to clean 377 Path parent = new Path(testDir, "parent"); 378 Path child = new Path(parent, "child"); 379 Path file = new Path(child, "someFile"); 380 fs.mkdirs(child); 381 382 // touch a new file 383 fs.create(file).close(); 384 assertTrue("Test file didn't get created.", fs.exists(file)); 385 386 // run the chore 387 chore.chore(); 388 389 // verify all the files got deleted 390 assertFalse("File didn't get deleted", fs.exists(file)); 391 assertFalse("Empty directory didn't get deleted", fs.exists(child)); 392 assertFalse("Empty directory didn't get deleted", fs.exists(parent)); 393 } 394 395 @Test 396 public void testDeleteFileWithCleanerDisabled() throws Exception { 397 Stoppable stop = new StoppableImplementation(); 398 Configuration conf = UTIL.getConfiguration(); 399 Path testDir = UTIL.getDataTestDir(); 400 FileSystem fs = UTIL.getTestFileSystem(); 401 String confKey = "hbase.test.cleaner.delegates"; 402 conf.set(confKey, AlwaysDelete.class.getName()); 403 404 AllValidPaths chore = 405 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 406 407 // Disable cleaner 408 chore.setEnabled(false); 409 410 // create the directory layout in the directory to clean 411 Path parent = new Path(testDir, "parent"); 412 Path child = new Path(parent, "child"); 413 Path file = new Path(child, "someFile"); 414 fs.mkdirs(child); 415 416 // touch a new file 417 fs.create(file).close(); 418 assertTrue("Test file didn't get created.", fs.exists(file)); 419 420 // run the chore 421 chore.chore(); 422 423 // verify all the files exist 424 assertTrue("File got deleted with cleaner disabled", fs.exists(file)); 425 assertTrue("Directory got deleted", fs.exists(child)); 426 assertTrue("Directory got deleted", fs.exists(parent)); 427 } 428 429 @Test 430 public void testOnConfigurationChange() throws Exception { 431 int availableProcessorNum = Runtime.getRuntime().availableProcessors(); 432 if (availableProcessorNum == 1) { // no need to run this test 433 return; 434 } 435 436 // have at least 2 available processors/cores 437 int initPoolSize = availableProcessorNum / 2; 438 int changedPoolSize = availableProcessorNum; 439 440 Stoppable stop = new StoppableImplementation(); 441 Configuration conf = UTIL.getConfiguration(); 442 Path testDir = UTIL.getDataTestDir(); 443 FileSystem fs = UTIL.getTestFileSystem(); 444 String confKey = "hbase.test.cleaner.delegates"; 445 conf.set(confKey, AlwaysDelete.class.getName()); 446 conf.set(CleanerChore.CHORE_POOL_SIZE, String.valueOf(initPoolSize)); 447 AllValidPaths chore = 448 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL); 449 chore.setEnabled(true); 450 // Create subdirs under testDir 451 int dirNums = 6; 452 Path[] subdirs = new Path[dirNums]; 453 for (int i = 0; i < dirNums; i++) { 454 subdirs[i] = new Path(testDir, "subdir-" + i); 455 fs.mkdirs(subdirs[i]); 456 } 457 // Under each subdirs create 6 files 458 for (Path subdir : subdirs) { 459 createFiles(fs, subdir, 6); 460 } 461 // Start chore 462 Thread t = new Thread(() -> chore.chore()); 463 t.setDaemon(true); 464 t.start(); 465 // Change size of chore's pool 466 conf.set(CleanerChore.CHORE_POOL_SIZE, String.valueOf(changedPoolSize)); 467 POOL.onConfigurationChange(conf); 468 assertEquals(changedPoolSize, chore.getChorePoolSize()); 469 // Stop chore 470 t.join(); 471 } 472 473 @Test 474 public void testOnConfigurationChangeLogCleaner() throws Exception { 475 int availableProcessorNum = Runtime.getRuntime().availableProcessors(); 476 if (availableProcessorNum == 1) { // no need to run this test 477 return; 478 } 479 480 DirScanPool pool = DirScanPool.getLogCleanerScanPool(UTIL.getConfiguration()); 481 482 // have at least 2 available processors/cores 483 int initPoolSize = availableProcessorNum / 2; 484 int changedPoolSize = availableProcessorNum; 485 486 Stoppable stop = new StoppableImplementation(); 487 Configuration conf = UTIL.getConfiguration(); 488 Path testDir = UTIL.getDataTestDir(); 489 FileSystem fs = UTIL.getTestFileSystem(); 490 String confKey = "hbase.test.cleaner.delegates"; 491 conf.set(confKey, AlwaysDelete.class.getName()); 492 conf.set(CleanerChore.LOG_CLEANER_CHORE_SIZE, String.valueOf(initPoolSize)); 493 final AllValidPaths chore = 494 new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, pool); 495 chore.setEnabled(true); 496 // Create subdirs under testDir 497 int dirNums = 6; 498 Path[] subdirs = new Path[dirNums]; 499 for (int i = 0; i < dirNums; i++) { 500 subdirs[i] = new Path(testDir, "subdir-" + i); 501 fs.mkdirs(subdirs[i]); 502 } 503 // Under each subdirs create 6 files 504 for (Path subdir : subdirs) { 505 createFiles(fs, subdir, 6); 506 } 507 // Start chore 508 Thread t = new Thread(new Runnable() { 509 @Override 510 public void run() { 511 chore.chore(); 512 } 513 }); 514 t.setDaemon(true); 515 t.start(); 516 // Change size of chore's pool 517 conf.set(CleanerChore.LOG_CLEANER_CHORE_SIZE, String.valueOf(changedPoolSize)); 518 pool.onConfigurationChange(conf); 519 assertEquals(changedPoolSize, chore.getChorePoolSize()); 520 // Stop chore 521 t.join(); 522 } 523 524 @Test 525 public void testMinimumNumberOfThreads() throws Exception { 526 Configuration conf = UTIL.getConfiguration(); 527 String confKey = "hbase.test.cleaner.delegates"; 528 conf.set(confKey, AlwaysDelete.class.getName()); 529 conf.set(CleanerChore.CHORE_POOL_SIZE, "2"); 530 int numProcs = Runtime.getRuntime().availableProcessors(); 531 // Sanity 532 assertEquals(numProcs, CleanerChore.calculatePoolSize(Integer.toString(numProcs))); 533 // The implementation does not allow us to set more threads than we have processors 534 assertEquals(numProcs, CleanerChore.calculatePoolSize(Integer.toString(numProcs + 2))); 535 // Force us into the branch that is multiplying 0.0 against the number of processors 536 assertEquals(1, CleanerChore.calculatePoolSize("0.0")); 537 } 538 539 private void createFiles(FileSystem fs, Path parentDir, int numOfFiles) throws IOException { 540 for (int i = 0; i < numOfFiles; i++) { 541 int xMega = 1 + ThreadLocalRandom.current().nextInt(3); // size of each file is between 1~3M 542 try (FSDataOutputStream fsdos = fs.create(new Path(parentDir, "file-" + i))) { 543 for (int m = 0; m < xMega; m++) { 544 byte[] M = new byte[1024 * 1024]; 545 Bytes.random(M); 546 fsdos.write(M); 547 } 548 } 549 } 550 } 551 552 private static class AllValidPaths extends CleanerChore<BaseHFileCleanerDelegate> { 553 554 public AllValidPaths(String name, Stoppable s, Configuration conf, FileSystem fs, 555 Path oldFileDir, String confkey, DirScanPool pool) { 556 super(name, Integer.MAX_VALUE, s, conf, fs, oldFileDir, confkey, pool); 557 } 558 559 // all paths are valid 560 @Override 561 protected boolean validate(Path file) { 562 return true; 563 } 564 }; 565 566 public static class AlwaysDelete extends BaseHFileCleanerDelegate { 567 @Override 568 public boolean isFileDeletable(FileStatus fStat) { 569 return true; 570 } 571 } 572 573 public static class NeverDelete extends BaseHFileCleanerDelegate { 574 @Override 575 public boolean isFileDeletable(FileStatus fStat) { 576 return false; 577 } 578 } 579}