View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.master.cleaner;
19  
20  import static org.junit.Assert.assertEquals;
21  import static org.junit.Assert.assertFalse;
22  import static org.junit.Assert.assertTrue;
23  
24  import java.io.IOException;
25  import java.util.Random;
26  import java.util.concurrent.atomic.AtomicBoolean;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.apache.hadoop.conf.Configuration;
31  import org.apache.hadoop.fs.FSDataOutputStream;
32  import org.apache.hadoop.fs.FileStatus;
33  import org.apache.hadoop.fs.FileSystem;
34  import org.apache.hadoop.fs.FilterFileSystem;
35  import org.apache.hadoop.fs.Path;
36  import org.apache.hadoop.hbase.HBaseTestingUtility;
37  import org.apache.hadoop.hbase.testclassification.SmallTests;
38  import org.apache.hadoop.hbase.Stoppable;
39  import org.apache.hadoop.hbase.util.FSUtils;
40  import org.apache.hadoop.hbase.util.StoppableImplementation;
41  import org.junit.AfterClass;
42  import org.junit.BeforeClass;
43  import org.junit.Test;
44  import org.junit.experimental.categories.Category;
45  import org.mockito.Mockito;
46  import org.mockito.invocation.InvocationOnMock;
47  import org.mockito.stubbing.Answer;
48  
49  @Category(SmallTests.class)
50  public class TestCleanerChore {
51  
52    private static final Log LOG = LogFactory.getLog(TestCleanerChore.class);
53    private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
54    private static DirScanPool POOL;
55  
56    @BeforeClass
57    public static void setup() {
58      POOL = new DirScanPool(UTIL.getConfiguration());
59    }
60  
61    @AfterClass
62    public static void cleanup() throws Exception {
63      // delete and recreate the test directory, ensuring a clean test dir between tests
64      UTIL.cleanupTestDir();
65      POOL.shutdownNow();
66    }
67  
68  
69    @Test
70    public void testSavesFilesOnRequest() throws Exception {
71      Stoppable stop = new StoppableImplementation();
72      Configuration conf = UTIL.getConfiguration();
73      Path testDir = UTIL.getDataTestDir();
74      FileSystem fs = UTIL.getTestFileSystem();
75      String confKey = "hbase.test.cleaner.delegates";
76      conf.set(confKey, NeverDelete.class.getName());
77  
78      AllValidPaths chore =
79        new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
80  
81      // create the directory layout in the directory to clean
82      Path parent = new Path(testDir, "parent");
83      Path file = new Path(parent, "someFile");
84      fs.mkdirs(parent);
85      // touch a new file
86      fs.create(file).close();
87      assertTrue("Test file didn't get created.", fs.exists(file));
88  
89      // run the chore
90      chore.chore();
91  
92      // verify all the files were preserved
93      assertTrue("File shouldn't have been deleted", fs.exists(file));
94      assertTrue("directory shouldn't have been deleted", fs.exists(parent));
95    }
96  
97    @Test
98    public void retriesIOExceptionInStatus() throws Exception {
99      Stoppable stop = new StoppableImplementation();
100     Configuration conf = UTIL.getConfiguration();
101     Path testDir = UTIL.getDataTestDir();
102     FileSystem fs = UTIL.getTestFileSystem();
103     String confKey = "hbase.test.cleaner.delegates";
104 
105     Path child = new Path(testDir, "child");
106     Path file = new Path(child, "file");
107     fs.mkdirs(child);
108     fs.create(file).close();
109     assertTrue("test file didn't get created.", fs.exists(file));
110     final AtomicBoolean fails = new AtomicBoolean(true);
111 
112     FilterFileSystem filtered = new FilterFileSystem(fs) {
113       public FileStatus[] listStatus(Path f) throws IOException {
114         if (fails.get()) {
115           throw new IOException("whomp whomp.");
116         }
117         return fs.listStatus(f);
118       }
119     };
120 
121     AllValidPaths chore =
122             new AllValidPaths("test-retry-ioe", stop, conf, filtered, testDir, confKey, POOL);
123 
124     // trouble talking to the filesystem
125     Boolean result = chore.runCleaner();
126 
127     // verify that it couldn't clean the files.
128     assertTrue("test rig failed to inject failure.", fs.exists(file));
129     assertTrue("test rig failed to inject failure.", fs.exists(child));
130     // and verify that it accurately reported the failure.
131     assertFalse("chore should report that it failed.", result);
132 
133     // filesystem is back
134     fails.set(false);
135     result = chore.runCleaner();
136 
137     // verify everything is gone.
138     assertFalse("file should have been destroyed.", fs.exists(file));
139     assertFalse("directory should have been destroyed.", fs.exists(child));
140     // and verify that it accurately reported success.
141     assertTrue("chore should claim it succeeded.", result);
142   }
143 
144   @Test
145   public void testDeletesEmptyDirectories() throws Exception {
146     Stoppable stop = new StoppableImplementation();
147     Configuration conf = UTIL.getConfiguration();
148     Path testDir = UTIL.getDataTestDir();
149     FileSystem fs = UTIL.getTestFileSystem();
150     String confKey = "hbase.test.cleaner.delegates";
151     conf.set(confKey, AlwaysDelete.class.getName());
152 
153     AllValidPaths chore =
154       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
155 
156     // create the directory layout in the directory to clean
157     Path parent = new Path(testDir, "parent");
158     Path child = new Path(parent, "child");
159     Path emptyChild = new Path(parent, "emptyChild");
160     Path file = new Path(child, "someFile");
161     fs.mkdirs(child);
162     fs.mkdirs(emptyChild);
163     // touch a new file
164     fs.create(file).close();
165     // also create a file in the top level directory
166     Path topFile = new Path(testDir, "topFile");
167     fs.create(topFile).close();
168     assertTrue("Test file didn't get created.", fs.exists(file));
169     assertTrue("Test file didn't get created.", fs.exists(topFile));
170 
171     // run the chore
172     chore.chore();
173 
174     // verify all the files got deleted
175     assertFalse("File didn't get deleted", fs.exists(topFile));
176     assertFalse("File didn't get deleted", fs.exists(file));
177     assertFalse("Empty directory didn't get deleted", fs.exists(child));
178     assertFalse("Empty directory didn't get deleted", fs.exists(parent));
179   }
180 
181   /**
182    * Test to make sure that we don't attempt to ask the delegate whether or not we should preserve a
183    * directory.
184    * @throws Exception on failure
185    */
186   @Test
187   public void testDoesNotCheckDirectories() throws Exception {
188     Stoppable stop = new StoppableImplementation();
189     Configuration conf = UTIL.getConfiguration();
190     Path testDir = UTIL.getDataTestDir();
191     FileSystem fs = UTIL.getTestFileSystem();
192     String confKey = "hbase.test.cleaner.delegates";
193     conf.set(confKey, AlwaysDelete.class.getName());
194 
195     AllValidPaths chore =
196       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
197     // spy on the delegate to ensure that we don't check for directories
198     AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0);
199     AlwaysDelete spy = Mockito.spy(delegate);
200     chore.cleanersChain.set(0, spy);
201 
202     // create the directory layout in the directory to clean
203     Path parent = new Path(testDir, "parent");
204     Path file = new Path(parent, "someFile");
205     fs.mkdirs(parent);
206     assertTrue("Test parent didn't get created.", fs.exists(parent));
207     // touch a new file
208     fs.create(file).close();
209     assertTrue("Test file didn't get created.", fs.exists(file));
210     
211     FileStatus fStat = fs.getFileStatus(parent);
212     chore.chore();
213     // make sure we never checked the directory
214     Mockito.verify(spy, Mockito.never()).isFileDeletable(fStat);
215     Mockito.reset(spy);
216   }
217 
218   @Test
219   public void testStoppedCleanerDoesNotDeleteFiles() throws Exception {
220     Stoppable stop = new StoppableImplementation();
221     Configuration conf = UTIL.getConfiguration();
222     Path testDir = UTIL.getDataTestDir();
223     FileSystem fs = UTIL.getTestFileSystem();
224     String confKey = "hbase.test.cleaner.delegates";
225     conf.set(confKey, AlwaysDelete.class.getName());
226 
227     AllValidPaths chore =
228       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
229 
230     // also create a file in the top level directory
231     Path topFile = new Path(testDir, "topFile");
232     fs.create(topFile).close();
233     assertTrue("Test file didn't get created.", fs.exists(topFile));
234 
235     // stop the chore
236     stop.stop("testing stop");
237 
238     // run the chore
239     chore.chore();
240 
241     // test that the file still exists
242     assertTrue("File got deleted while chore was stopped", fs.exists(topFile));
243   }
244 
245   /**
246    * While cleaning a directory, all the files in the directory may be deleted, but there may be
247    * another file added, in which case the directory shouldn't be deleted.
248    * @throws IOException on failure
249    */
250   @Test
251   public void testCleanerDoesNotDeleteDirectoryWithLateAddedFiles() throws IOException {
252     Stoppable stop = new StoppableImplementation();
253     Configuration conf = UTIL.getConfiguration();
254     final Path testDir = UTIL.getDataTestDir();
255     final FileSystem fs = UTIL.getTestFileSystem();
256     String confKey = "hbase.test.cleaner.delegates";
257     conf.set(confKey, AlwaysDelete.class.getName());
258 
259     AllValidPaths chore =
260       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
261     // spy on the delegate to ensure that we don't check for directories
262     AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0);
263     AlwaysDelete spy = Mockito.spy(delegate);
264     chore.cleanersChain.set(0, spy);
265 
266     // create the directory layout in the directory to clean
267     final Path parent = new Path(testDir, "parent");
268     Path file = new Path(parent, "someFile");
269     fs.mkdirs(parent);
270     // touch a new file
271     fs.create(file).close();
272     assertTrue("Test file didn't get created.", fs.exists(file));
273     final Path addedFile = new Path(parent, "addedFile");
274 
275     // when we attempt to delete the original file, add another file in the same directory
276     Mockito.doAnswer(new Answer<Boolean>() {
277       @Override
278       public Boolean answer(InvocationOnMock invocation) throws Throwable {
279         fs.create(addedFile).close();
280         FSUtils.logFileSystemState(fs, testDir, LOG);
281         return (Boolean) invocation.callRealMethod();
282       }
283     }).when(spy).isFileDeletable(Mockito.any(FileStatus.class));
284 
285     // run the chore
286     chore.chore();
287 
288     // make sure all the directories + added file exist, but the original file is deleted
289     assertTrue("Added file unexpectedly deleted", fs.exists(addedFile));
290     assertTrue("Parent directory deleted unexpectedly", fs.exists(parent));
291     assertFalse("Original file unexpectedly retained", fs.exists(file));
292     Mockito.verify(spy, Mockito.times(1)).isFileDeletable(Mockito.any(FileStatus.class));
293     Mockito.reset(spy);
294   }
295 
296   /**
297    * The cleaner runs in a loop, where it first checks to see all the files under a directory can be
298    * deleted. If they all can, then we try to delete the directory. However, a file may be added
299    * that directory to after the original check. This ensures that we don't accidentally delete that
300    * directory on and don't get spurious IOExceptions.
301    * <p>
302    * This was from HBASE-7465.
303    * @throws Exception on failure
304    */
305   @Test
306   public void testNoExceptionFromDirectoryWithRacyChildren() throws Exception {
307     UTIL.cleanupTestDir();
308     Stoppable stop = new StoppableImplementation();
309     // need to use a localutil to not break the rest of the test that runs on the local FS, which
310     // gets hosed when we start to use a minicluster.
311     HBaseTestingUtility localUtil = new HBaseTestingUtility();
312     Configuration conf = localUtil.getConfiguration();
313     final Path testDir = UTIL.getDataTestDir();
314     final FileSystem fs = UTIL.getTestFileSystem();
315     LOG.debug("Writing test data to: " + testDir);
316     String confKey = "hbase.test.cleaner.delegates";
317     conf.set(confKey, AlwaysDelete.class.getName());
318 
319     AllValidPaths chore =
320       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
321     // spy on the delegate to ensure that we don't check for directories
322     AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0);
323     AlwaysDelete spy = Mockito.spy(delegate);
324     chore.cleanersChain.set(0, spy);
325 
326     // create the directory layout in the directory to clean
327     final Path parent = new Path(testDir, "parent");
328     Path file = new Path(parent, "someFile");
329     fs.mkdirs(parent);
330     // touch a new file
331     fs.create(file).close();
332     assertTrue("Test file didn't get created.", fs.exists(file));
333     final Path racyFile = new Path(parent, "addedFile");
334 
335     // when we attempt to delete the original file, add another file in the same directory
336     Mockito.doAnswer(new Answer<Boolean>() {
337       @Override
338       public Boolean answer(InvocationOnMock invocation) throws Throwable {
339         fs.create(racyFile).close();
340         FSUtils.logFileSystemState(fs, testDir, LOG);
341         return (Boolean) invocation.callRealMethod();
342       }
343     }).when(spy).isFileDeletable(Mockito.any(FileStatus.class));
344 
345     // run the chore
346     chore.chore();
347 
348     // make sure all the directories + added file exist, but the original file is deleted
349     assertTrue("Added file unexpectedly deleted", fs.exists(racyFile));
350     assertTrue("Parent directory deleted unexpectedly", fs.exists(parent));
351     assertFalse("Original file unexpectedly retained", fs.exists(file));
352     Mockito.verify(spy, Mockito.times(1)).isFileDeletable(Mockito.any(FileStatus.class));
353   }
354 
355   @Test
356   public void testDeleteFileWithCleanerEnabled() throws Exception {
357     Stoppable stop = new StoppableImplementation();
358     Configuration conf = UTIL.getConfiguration();
359     Path testDir = UTIL.getDataTestDir();
360     FileSystem fs = UTIL.getTestFileSystem();
361     String confKey = "hbase.test.cleaner.delegates";
362     conf.set(confKey, AlwaysDelete.class.getName());
363 
364     AllValidPaths chore =
365       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
366 
367     // Enable cleaner
368     chore.setEnabled(true);
369 
370     // create the directory layout in the directory to clean
371     Path parent = new Path(testDir, "parent");
372     Path child = new Path(parent, "child");
373     Path file = new Path(child, "someFile");
374     fs.mkdirs(child);
375 
376     // touch a new file
377     fs.create(file).close();
378     assertTrue("Test file didn't get created.", fs.exists(file));
379 
380     // run the chore
381     chore.chore();
382 
383     // verify all the files got deleted
384     assertFalse("File didn't get deleted", fs.exists(file));
385     assertFalse("Empty directory didn't get deleted", fs.exists(child));
386     assertFalse("Empty directory didn't get deleted", fs.exists(parent));
387   }
388 
389   @Test
390   public void testDeleteFileWithCleanerDisabled() throws Exception {
391     Stoppable stop = new StoppableImplementation();
392     Configuration conf = UTIL.getConfiguration();
393     Path testDir = UTIL.getDataTestDir();
394     FileSystem fs = UTIL.getTestFileSystem();
395     String confKey = "hbase.test.cleaner.delegates";
396     conf.set(confKey, AlwaysDelete.class.getName());
397 
398     AllValidPaths chore =
399       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
400 
401     // Disable cleaner
402     chore.setEnabled(false);
403 
404     // create the directory layout in the directory to clean
405     Path parent = new Path(testDir, "parent");
406     Path child = new Path(parent, "child");
407     Path file = new Path(child, "someFile");
408     fs.mkdirs(child);
409 
410     // touch a new file
411     fs.create(file).close();
412     assertTrue("Test file didn't get created.", fs.exists(file));
413 
414     // run the chore
415     chore.chore();
416 
417     // verify all the files got deleted
418     assertTrue("File got deleted with cleaner disabled", fs.exists(file));
419     assertTrue("Directory got deleted", fs.exists(child));
420     assertTrue("Directory got deleted", fs.exists(parent));
421   }
422 
423 
424   @Test
425   public void testOnConfigurationChange() throws Exception {
426     int availableProcessorNum = Runtime.getRuntime().availableProcessors();
427     if (availableProcessorNum == 1) { // no need to run this test
428       return;
429     }
430 
431     // have at least 2 available processors/cores
432     int initPoolSize = availableProcessorNum / 2;
433     int changedPoolSize = availableProcessorNum;
434 
435     Stoppable stop = new StoppableImplementation();
436     Configuration conf = UTIL.getConfiguration();
437     Path testDir = UTIL.getDataTestDir();
438     FileSystem fs = UTIL.getTestFileSystem();
439     String confKey = "hbase.test.cleaner.delegates";
440     conf.set(confKey, AlwaysDelete.class.getName());
441     conf.set(CleanerChore.CHORE_POOL_SIZE, String.valueOf(initPoolSize));
442     final AllValidPaths chore =
443       new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey, POOL);
444     chore.setEnabled(true);
445     // Create subdirs under testDir
446     int dirNums = 6;
447     Path[] subdirs = new Path[dirNums];
448     for (int i = 0; i < dirNums; i++) {
449       subdirs[i] = new Path(testDir, "subdir-" + i);
450       fs.mkdirs(subdirs[i]);
451     }
452     // Under each subdirs create 6 files
453     for (Path subdir : subdirs) {
454       createFiles(fs, subdir, 6);
455     }
456     // Start chore
457     Thread t = new Thread(new Runnable() {
458       @Override
459       public void run() {
460         chore.chore();
461       }
462     });
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 testMinimumNumberOfThreads() throws Exception {
475     Stoppable stop = new StoppableImplementation();
476     Configuration conf = UTIL.getConfiguration();
477     Path testDir = UTIL.getDataTestDir();
478     FileSystem fs = UTIL.getTestFileSystem();
479     String confKey = "hbase.test.cleaner.delegates";
480     conf.set(confKey, AlwaysDelete.class.getName());
481     conf.set(CleanerChore.CHORE_POOL_SIZE, "2");
482     int numProcs = Runtime.getRuntime().availableProcessors();
483     // Sanity
484     assertEquals(numProcs, CleanerChore.calculatePoolSize(Integer.toString(numProcs)));
485     // The implementation does not allow us to set more threads than we have processors
486     assertEquals(numProcs, CleanerChore.calculatePoolSize(Integer.toString(numProcs + 2)));
487     // Force us into the branch that is multiplying 0.0 against the number of processors
488     assertEquals(1, CleanerChore.calculatePoolSize("0.0"));
489   }
490 
491   private void createFiles(FileSystem fs, Path parentDir, int numOfFiles) throws IOException {
492     Random random = new Random();
493     for (int i = 0; i < numOfFiles; i++) {
494       int xMega = 1 + random.nextInt(3); // size of each file is between 1~3M
495       try (FSDataOutputStream fsdos = fs.create(new Path(parentDir, "file-" + i))) {
496         for (int m = 0; m < xMega; m++) {
497           byte[] M = new byte[1024 * 1024];
498           random.nextBytes(M);
499           fsdos.write(M);
500         }
501       }
502     }
503   }
504 
505   private static class AllValidPaths extends CleanerChore<BaseHFileCleanerDelegate> {
506 
507     public AllValidPaths(String name, Stoppable s, Configuration conf, FileSystem fs,
508       Path oldFileDir, String confkey, DirScanPool pool) {
509       super(name, Integer.MAX_VALUE, s, conf, fs, oldFileDir, confkey, pool);
510     }
511 
512     // all paths are valid
513     @Override
514     protected boolean validate(Path file) {
515       return true;
516     }
517   };
518 
519   public static class AlwaysDelete extends BaseHFileCleanerDelegate {
520     @Override
521     public boolean isFileDeletable(FileStatus fStat) {
522       return true;
523     }
524   }
525 
526   public static class NeverDelete extends BaseHFileCleanerDelegate {
527     @Override
528     public boolean isFileDeletable(FileStatus fStat) {
529       return false;
530     }
531   }
532 }