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}