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}