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