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