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.assertFalse;
21  import static org.junit.Assert.assertTrue;
22  
23  import java.io.IOException;
24  
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.apache.hadoop.conf.Configuration;
28  import org.apache.hadoop.fs.FileSystem;
29  import org.apache.hadoop.fs.Path;
30  import org.apache.hadoop.hbase.HBaseTestingUtility;
31  import org.apache.hadoop.hbase.SmallTests;
32  import org.apache.hadoop.hbase.Stoppable;
33  import org.apache.hadoop.hbase.util.FSUtils;
34  import org.junit.After;
35  import org.junit.Test;
36  import org.junit.experimental.categories.Category;
37  import org.mockito.Mockito;
38  import org.mockito.invocation.InvocationOnMock;
39  import org.mockito.stubbing.Answer;
40  
41  @Category(SmallTests.class)
42  public class TestCleanerChore {
43  
44    private static final Log LOG = LogFactory.getLog(TestCleanerChore.class);
45    private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
46  
47    @After
48    public void cleanup() throws Exception {
49      // delete and recreate the test directory, ensuring a clean test dir between tests
50      UTIL.cleanupTestDir();
51    }
52  
53    @Test
54    public void testSavesFilesOnRequest() throws Exception {
55      Stoppable stop = new StoppableImplementation();
56      Configuration conf = UTIL.getConfiguration();
57      Path testDir = UTIL.getDataTestDir();
58      FileSystem fs = UTIL.getTestFileSystem();
59      String confKey = "hbase.test.cleaner.delegates";
60      conf.set(confKey, NeverDelete.class.getName());
61  
62      AllValidPaths chore = new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey);
63  
64      // create the directory layout in the directory to clean
65      Path parent = new Path(testDir, "parent");
66      Path file = new Path(parent, "someFile");
67      fs.mkdirs(parent);
68      // touch a new file
69      fs.create(file).close();
70      assertTrue("Test file didn't get created.", fs.exists(file));
71  
72      // run the chore
73      chore.chore();
74  
75      // verify all the files got deleted
76      assertTrue("File didn't get deleted", fs.exists(file));
77      assertTrue("Empty directory didn't get deleted", fs.exists(parent));
78    }
79  
80    @Test
81    public void testDeletesEmptyDirectories() throws Exception {
82      Stoppable stop = new StoppableImplementation();
83      Configuration conf = UTIL.getConfiguration();
84      Path testDir = UTIL.getDataTestDir();
85      FileSystem fs = UTIL.getTestFileSystem();
86      String confKey = "hbase.test.cleaner.delegates";
87      conf.set(confKey, AlwaysDelete.class.getName());
88  
89      AllValidPaths chore = new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey);
90  
91      // create the directory layout in the directory to clean
92      Path parent = new Path(testDir, "parent");
93      Path child = new Path(parent, "child");
94      Path emptyChild = new Path(parent, "emptyChild");
95      Path file = new Path(child, "someFile");
96      fs.mkdirs(child);
97      fs.mkdirs(emptyChild);
98      // touch a new file
99      fs.create(file).close();
100     // also create a file in the top level directory
101     Path topFile = new Path(testDir, "topFile");
102     fs.create(topFile).close();
103     assertTrue("Test file didn't get created.", fs.exists(file));
104     assertTrue("Test file didn't get created.", fs.exists(topFile));
105 
106     // run the chore
107     chore.chore();
108 
109     // verify all the files got deleted
110     assertFalse("File didn't get deleted", fs.exists(topFile));
111     assertFalse("File didn't get deleted", fs.exists(file));
112     assertFalse("Empty directory didn't get deleted", fs.exists(child));
113     assertFalse("Empty directory didn't get deleted", fs.exists(parent));
114   }
115 
116   /**
117    * Test to make sure that we don't attempt to ask the delegate whether or not we should preserve a
118    * directory.
119    * @throws Exception on failure
120    */
121   @Test
122   public void testDoesNotCheckDirectories() throws Exception {
123     Stoppable stop = new StoppableImplementation();
124     Configuration conf = UTIL.getConfiguration();
125     Path testDir = UTIL.getDataTestDir();
126     FileSystem fs = UTIL.getTestFileSystem();
127     String confKey = "hbase.test.cleaner.delegates";
128     conf.set(confKey, AlwaysDelete.class.getName());
129 
130     AllValidPaths chore = new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey);
131     // spy on the delegate to ensure that we don't check for directories
132     AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0);
133     AlwaysDelete spy = Mockito.spy(delegate);
134     chore.cleanersChain.set(0, spy);
135 
136     // create the directory layout in the directory to clean
137     Path parent = new Path(testDir, "parent");
138     Path file = new Path(parent, "someFile");
139     fs.mkdirs(parent);
140     // touch a new file
141     fs.create(file).close();
142     assertTrue("Test file didn't get created.", fs.exists(file));
143 
144     chore.chore();
145     // make sure we never checked the directory
146     Mockito.verify(spy, Mockito.never()).isFileDeletable(parent);
147     Mockito.reset(spy);
148   }
149 
150   @Test
151   public void testStoppedCleanerDoesNotDeleteFiles() 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 = new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey);
160 
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(topFile));
165 
166     // stop the chore
167     stop.stop("testing stop");
168 
169     // run the chore
170     chore.chore();
171 
172     // test that the file still exists
173     assertTrue("File got deleted while chore was stopped", fs.exists(topFile));
174   }
175 
176   /**
177    * While cleaning a directory, all the files in the directory may be deleted, but there may be
178    * another file added, in which case the directory shouldn't be deleted.
179    * @throws IOException on failure
180    */
181   @Test
182   public void testCleanerDoesNotDeleteDirectoryWithLateAddedFiles() throws IOException {
183     Stoppable stop = new StoppableImplementation();
184     Configuration conf = UTIL.getConfiguration();
185     final Path testDir = UTIL.getDataTestDir();
186     final FileSystem fs = UTIL.getTestFileSystem();
187     String confKey = "hbase.test.cleaner.delegates";
188     conf.set(confKey, AlwaysDelete.class.getName());
189 
190     AllValidPaths chore = new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey);
191     // spy on the delegate to ensure that we don't check for directories
192     AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0);
193     AlwaysDelete spy = Mockito.spy(delegate);
194     chore.cleanersChain.set(0, spy);
195 
196     // create the directory layout in the directory to clean
197     final Path parent = new Path(testDir, "parent");
198     Path file = new Path(parent, "someFile");
199     fs.mkdirs(parent);
200     // touch a new file
201     fs.create(file).close();
202     assertTrue("Test file didn't get created.", fs.exists(file));
203     final Path addedFile = new Path(parent, "addedFile");
204 
205     // when we attempt to delete the original file, add another file in the same directory
206     Mockito.doAnswer(new Answer<Boolean>() {
207       @Override
208       public Boolean answer(InvocationOnMock invocation) throws Throwable {
209         fs.create(addedFile).close();
210         FSUtils.logFileSystemState(fs, testDir, LOG);
211         return (Boolean) invocation.callRealMethod();
212       }
213     }).when(spy).isFileDeletable(Mockito.any(Path.class));
214 
215     // run the chore
216     chore.chore();
217 
218     // make sure all the directories + added file exist, but the original file is deleted
219     assertTrue("Added file unexpectedly deleted", fs.exists(addedFile));
220     assertTrue("Parent directory deleted unexpectedly", fs.exists(parent));
221     assertFalse("Original file unexpectedly retained", fs.exists(file));
222     Mockito.verify(spy, Mockito.times(1)).isFileDeletable(Mockito.any(Path.class));
223     Mockito.reset(spy);
224   }
225   
226   /**
227    * The cleaner runs in a loop, where it first checks to see all the files under a directory can be
228    * deleted. If they all can, then we try to delete the directory. However, a file may be added
229    * that directory to after the original check. This ensures that we don't accidentally delete that
230    * directory on and don't get spurious IOExceptions.
231    * <p>
232    * This was from HBASE-7465.
233    * @throws Exception on failure
234    */
235   @Test
236   public void testNoExceptionFromDirectoryWithRacyChildren() throws Exception {
237     Stoppable stop = new StoppableImplementation();
238     // need to use a localutil to not break the rest of the test that runs on the local FS, which
239     // gets hosed when we start to use a minicluster.
240     HBaseTestingUtility localUtil = new HBaseTestingUtility();
241     Configuration conf = localUtil.getConfiguration();
242     final Path testDir = UTIL.getDataTestDir();
243     final FileSystem fs = UTIL.getTestFileSystem();
244     LOG.debug("Writing test data to: " + testDir);
245     String confKey = "hbase.test.cleaner.delegates";
246     conf.set(confKey, AlwaysDelete.class.getName());
247 
248     AllValidPaths chore = new AllValidPaths("test-file-cleaner", stop, conf, fs, testDir, confKey);
249     // spy on the delegate to ensure that we don't check for directories
250     AlwaysDelete delegate = (AlwaysDelete) chore.cleanersChain.get(0);
251     AlwaysDelete spy = Mockito.spy(delegate);
252     chore.cleanersChain.set(0, spy);
253 
254     // create the directory layout in the directory to clean
255     final Path parent = new Path(testDir, "parent");
256     Path file = new Path(parent, "someFile");
257     fs.mkdirs(parent);
258     // touch a new file
259     fs.create(file).close();
260     assertTrue("Test file didn't get created.", fs.exists(file));
261     final Path racyFile = new Path(parent, "addedFile");
262 
263     // when we attempt to delete the original file, add another file in the same directory
264     Mockito.doAnswer(new Answer<Boolean>() {
265       @Override
266       public Boolean answer(InvocationOnMock invocation) throws Throwable {
267         fs.create(racyFile).close();
268         FSUtils.logFileSystemState(fs, testDir, LOG);
269         return (Boolean) invocation.callRealMethod();
270       }
271     }).when(spy).isFileDeletable(Mockito.any(Path.class));
272 
273     // attempt to delete the directory, which
274     if (chore.checkAndDeleteDirectory(parent)) {
275       throw new Exception(
276           "Reported success deleting directory, should have failed when adding file mid-iteration");
277     }
278 
279     // make sure all the directories + added file exist, but the original file is deleted
280     assertTrue("Added file unexpectedly deleted", fs.exists(racyFile));
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(Path.class));
284   }
285 
286   private static class AllValidPaths extends CleanerChore<BaseHFileCleanerDelegate> {
287 
288     public AllValidPaths(String name, Stoppable s, Configuration conf, FileSystem fs,
289         Path oldFileDir, String confkey) {
290       super(name, Integer.MAX_VALUE, s, conf, fs, oldFileDir, confkey);
291     }
292 
293     // all paths are valid
294     @Override
295     protected boolean validate(Path file) {
296       return true;
297     }
298   };
299 
300   public static class AlwaysDelete extends BaseHFileCleanerDelegate {
301     @Override
302     public boolean isFileDeletable(Path file) {
303       return true;
304     }
305   }
306 
307   public static class NeverDelete extends BaseHFileCleanerDelegate {
308     @Override
309     public boolean isFileDeletable(Path file) {
310       return false;
311     }
312   }
313 
314   /**
315    * Simple helper class that just keeps track of whether or not its stopped.
316    */
317   private static class StoppableImplementation implements Stoppable {
318     private volatile boolean stop;
319 
320     @Override
321     public void stop(String why) {
322       this.stop = true;
323     }
324 
325     @Override
326     public boolean isStopped() {
327       return this.stop;
328     }
329 
330   }
331 }