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.List;
26  import java.util.Random;
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.Path;
35  import org.apache.hadoop.hbase.ChoreService;
36  import org.apache.hadoop.hbase.CoordinatedStateManager;
37  import org.apache.hadoop.hbase.HBaseTestingUtility;
38  import org.apache.hadoop.hbase.HConstants;
39  import org.apache.hadoop.hbase.Server;
40  import org.apache.hadoop.hbase.ServerName;
41  import org.apache.hadoop.hbase.client.ClusterConnection;
42  import org.apache.hadoop.hbase.testclassification.MediumTests;
43  import org.apache.hadoop.hbase.util.EnvironmentEdge;
44  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
45  import org.apache.hadoop.hbase.zookeeper.MetaTableLocator;
46  import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
47  import org.junit.AfterClass;
48  import org.junit.Assert;
49  import org.junit.BeforeClass;
50  import org.junit.Test;
51  import org.junit.experimental.categories.Category;
52  
53  @Category(MediumTests.class)
54  public class TestHFileCleaner {
55    private static final Log LOG = LogFactory.getLog(TestHFileCleaner.class);
56  
57    private final static HBaseTestingUtility UTIL = new HBaseTestingUtility();
58  
59    @BeforeClass
60    public static void setupCluster() throws Exception {
61      // have to use a minidfs cluster because the localfs doesn't modify file times correctly
62      UTIL.startMiniDFSCluster(1);
63      CleanerChore.initChorePool(UTIL.getConfiguration());
64    }
65  
66    @AfterClass
67    public static void shutdownCluster() throws IOException {
68      UTIL.shutdownMiniDFSCluster();
69    }
70  
71    @Test
72    public void testTTLCleaner() throws IOException, InterruptedException {
73      FileSystem fs = UTIL.getDFSCluster().getFileSystem();
74      Path root = UTIL.getDataTestDirOnTestFS();
75      Path file = new Path(root, "file");
76      fs.createNewFile(file);
77      long createTime = System.currentTimeMillis();
78      assertTrue("Test file not created!", fs.exists(file));
79      TimeToLiveHFileCleaner cleaner = new TimeToLiveHFileCleaner();
80      // update the time info for the file, so the cleaner removes it
81      fs.setTimes(file, createTime - 100, -1);
82      Configuration conf = UTIL.getConfiguration();
83      conf.setLong(TimeToLiveHFileCleaner.TTL_CONF_KEY, 100);
84      cleaner.setConf(conf);
85      assertTrue("File not set deletable - check mod time:" + getFileStats(file, fs)
86          + " with create time:" + createTime, cleaner.isFileDeletable(fs.getFileStatus(file)));
87    }
88  
89    /**
90     * @param file to check
91     * @return loggable information about the file
92     */
93    private String getFileStats(Path file, FileSystem fs) throws IOException {
94      FileStatus status = fs.getFileStatus(file);
95      return "File" + file + ", mtime:" + status.getModificationTime() + ", atime:"
96          + status.getAccessTime();
97    }
98  
99    @Test(timeout = 60 *1000)
100   public void testHFileCleaning() throws Exception {
101     final EnvironmentEdge originalEdge = EnvironmentEdgeManager.getDelegate();
102     String prefix = "someHFileThatWouldBeAUUID";
103     Configuration conf = UTIL.getConfiguration();
104     // set TTL
105     long ttl = 2000;
106     conf.set(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS,
107       "org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner");
108     conf.setLong(TimeToLiveHFileCleaner.TTL_CONF_KEY, ttl);
109     Server server = new DummyServer();
110     Path archivedHfileDir = new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
111     FileSystem fs = FileSystem.get(conf);
112     HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir);
113 
114     // Create 2 invalid files, 1 "recent" file, 1 very new file and 30 old files
115     final long createTime = System.currentTimeMillis();
116     fs.delete(archivedHfileDir, true);
117     fs.mkdirs(archivedHfileDir);
118     // Case 1: 1 invalid file, which should be deleted directly
119     fs.createNewFile(new Path(archivedHfileDir, "dfd-dfd"));
120     // Case 2: 1 "recent" file, not even deletable for the first log cleaner
121     // (TimeToLiveLogCleaner), so we are not going down the chain
122     LOG.debug("Now is: " + createTime);
123     for (int i = 1; i < 32; i++) {
124       // Case 3: old files which would be deletable for the first log cleaner
125       // (TimeToLiveHFileCleaner),
126       Path fileName = new Path(archivedHfileDir, (prefix + "." + (createTime + i)));
127       fs.createNewFile(fileName);
128       // set the creation time past ttl to ensure that it gets removed
129       fs.setTimes(fileName, createTime - ttl - 1, -1);
130       LOG.debug("Creating " + getFileStats(fileName, fs));
131     }
132 
133     // Case 2: 1 newer file, not even deletable for the first log cleaner
134     // (TimeToLiveLogCleaner), so we are not going down the chain
135     Path saved = new Path(archivedHfileDir, prefix + ".00000000000");
136     fs.createNewFile(saved);
137     // set creation time within the ttl
138     fs.setTimes(saved, createTime - ttl / 2, -1);
139     LOG.debug("Creating " + getFileStats(saved, fs));
140     for (FileStatus stat : fs.listStatus(archivedHfileDir)) {
141       LOG.debug(stat.getPath().toString());
142     }
143 
144     assertEquals(33, fs.listStatus(archivedHfileDir).length);
145 
146     // set a custom edge manager to handle time checking
147     EnvironmentEdge setTime = new EnvironmentEdge() {
148       @Override
149       public long currentTime() {
150         return createTime;
151       }
152     };
153     EnvironmentEdgeManager.injectEdge(setTime);
154 
155     // run the chore
156     cleaner.chore();
157 
158     // ensure we only end up with the saved file
159     assertEquals(1, fs.listStatus(archivedHfileDir).length);
160 
161     for (FileStatus file : fs.listStatus(archivedHfileDir)) {
162       LOG.debug("Kept hfiles: " + file.getPath().getName());
163     }
164 
165     // reset the edge back to the original edge
166     EnvironmentEdgeManager.injectEdge(originalEdge);
167   }
168 
169   @Test
170   public void testRemovesEmptyDirectories() throws Exception {
171     Configuration conf = UTIL.getConfiguration();
172     // no cleaner policies = delete all files
173     conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
174     Server server = new DummyServer();
175     Path archivedHfileDir = new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
176 
177     // setup the cleaner
178     FileSystem fs = UTIL.getDFSCluster().getFileSystem();
179     HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir);
180 
181     // make all the directories for archiving files
182     Path table = new Path(archivedHfileDir, "table");
183     Path region = new Path(table, "regionsomthing");
184     Path family = new Path(region, "fam");
185     Path file = new Path(family, "file12345");
186     fs.mkdirs(family);
187     if (!fs.exists(family)) throw new RuntimeException("Couldn't create test family:" + family);
188     fs.create(file).close();
189     if (!fs.exists(file)) throw new RuntimeException("Test file didn't get created:" + file);
190 
191     // run the chore to cleanup the files (and the directories above it)
192     cleaner.chore();
193 
194     // make sure all the parent directories get removed
195     assertFalse("family directory not removed for empty directory", fs.exists(family));
196     assertFalse("region directory not removed for empty directory", fs.exists(region));
197     assertFalse("table directory not removed for empty directory", fs.exists(table));
198     assertTrue("archive directory", fs.exists(archivedHfileDir));
199   }
200 
201   static class DummyServer implements Server {
202 
203     @Override
204     public Configuration getConfiguration() {
205       return UTIL.getConfiguration();
206     }
207 
208     @Override
209     public ZooKeeperWatcher getZooKeeper() {
210       try {
211         return new ZooKeeperWatcher(getConfiguration(), "dummy server", this);
212       } catch (IOException e) {
213         e.printStackTrace();
214       }
215       return null;
216     }
217 
218     @Override
219     public CoordinatedStateManager getCoordinatedStateManager() {
220       return null;
221     }
222 
223     @Override
224     public ClusterConnection getConnection() {
225       return null;
226     }
227 
228     @Override
229     public MetaTableLocator getMetaTableLocator() {
230       return null;
231     }
232 
233     @Override
234     public ServerName getServerName() {
235       return ServerName.valueOf("regionserver,60020,000000");
236     }
237 
238     @Override
239     public void abort(String why, Throwable e) {
240     }
241 
242     @Override
243     public boolean isAborted() {
244       return false;
245     }
246 
247     @Override
248     public void stop(String why) {
249     }
250 
251     @Override
252     public boolean isStopped() {
253       return false;
254     }
255 
256     @Override
257     public ChoreService getChoreService() {
258       return null;
259     }
260   }
261 
262   @Test
263   public void testThreadCleanup() throws Exception {
264     Configuration conf = UTIL.getConfiguration();
265     conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
266     Server server = new DummyServer();
267     Path archivedHfileDir =
268         new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
269 
270     // setup the cleaner
271     FileSystem fs = UTIL.getDFSCluster().getFileSystem();
272     HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir);
273     // clean up archive directory
274     fs.delete(archivedHfileDir, true);
275     fs.mkdirs(archivedHfileDir);
276     // create some file to delete
277     fs.createNewFile(new Path(archivedHfileDir, "dfd-dfd"));
278     // launch the chore
279     cleaner.chore();
280     // call cleanup
281     cleaner.cleanup();
282     // wait awhile for thread to die
283     Thread.sleep(100);
284     for (Thread thread : cleaner.getCleanerThreads()) {
285       Assert.assertFalse(thread.isAlive());
286     }
287   }
288 
289   @Test
290   public void testLargeSmallIsolation() throws Exception {
291     Configuration conf = UTIL.getConfiguration();
292     // no cleaner policies = delete all files
293     conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
294     conf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, 512 * 1024);
295     Server server = new DummyServer();
296     Path archivedHfileDir =
297         new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
298 
299     // setup the cleaner
300     FileSystem fs = UTIL.getDFSCluster().getFileSystem();
301     HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir);
302     // clean up archive directory
303     fs.delete(archivedHfileDir, true);
304     fs.mkdirs(archivedHfileDir);
305     // necessary set up
306     final int LARGE_FILE_NUM = 5;
307     final int SMALL_FILE_NUM = 20;
308     createFilesForTesting(LARGE_FILE_NUM, SMALL_FILE_NUM, fs, archivedHfileDir);
309     // call cleanup
310     cleaner.chore();
311 
312     Assert.assertEquals(LARGE_FILE_NUM, cleaner.getNumOfDeletedLargeFiles());
313     Assert.assertEquals(SMALL_FILE_NUM, cleaner.getNumOfDeletedSmallFiles());
314   }
315 
316   @Test(timeout = 60 * 1000)
317   public void testOnConfigurationChange() throws Exception {
318     // constants
319     final int ORIGINAL_THROTTLE_POINT = 512 * 1024;
320     final int ORIGINAL_QUEUE_INIT_SIZE = 512;
321     final int UPDATE_THROTTLE_POINT = 1024;// small enough to change large/small check
322     final int UPDATE_QUEUE_INIT_SIZE = 1024;
323     final int LARGE_FILE_NUM = 5;
324     final int SMALL_FILE_NUM = 20;
325     final int LARGE_THREAD_NUM = 2;
326     final int SMALL_THREAD_NUM = 4;
327     final long THREAD_TIMEOUT_MSEC = 30 * 1000L;
328     final long THREAD_CHECK_INTERVAL_MSEC = 500L;
329 
330     Configuration conf = UTIL.getConfiguration();
331     // no cleaner policies = delete all files
332     conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, "");
333     conf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, ORIGINAL_THROTTLE_POINT);
334     conf.setInt(HFileCleaner.LARGE_HFILE_QUEUE_INIT_SIZE, ORIGINAL_QUEUE_INIT_SIZE);
335     conf.setInt(HFileCleaner.SMALL_HFILE_QUEUE_INIT_SIZE, ORIGINAL_QUEUE_INIT_SIZE);
336     Server server = new DummyServer();
337     Path archivedHfileDir =
338         new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY);
339 
340     // setup the cleaner
341     FileSystem fs = UTIL.getDFSCluster().getFileSystem();
342     final HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir);
343     Assert.assertEquals(ORIGINAL_THROTTLE_POINT, cleaner.getThrottlePoint());
344     Assert.assertEquals(ORIGINAL_QUEUE_INIT_SIZE, cleaner.getLargeQueueInitSize());
345     Assert.assertEquals(ORIGINAL_QUEUE_INIT_SIZE, cleaner.getSmallQueueInitSize());
346     Assert.assertEquals(HFileCleaner.DEFAULT_HFILE_DELETE_THREAD_TIMEOUT_MSEC,
347         cleaner.getCleanerThreadTimeoutMsec());
348     Assert.assertEquals(HFileCleaner.DEFAULT_HFILE_DELETE_THREAD_CHECK_INTERVAL_MSEC,
349         cleaner.getCleanerThreadCheckIntervalMsec());
350 
351     // clean up archive directory and create files for testing
352     fs.delete(archivedHfileDir, true);
353     fs.mkdirs(archivedHfileDir);
354     createFilesForTesting(LARGE_FILE_NUM, SMALL_FILE_NUM, fs, archivedHfileDir);
355 
356     // call cleaner, run as daemon to test the interrupt-at-middle case
357     Thread t = new Thread() {
358       @Override
359       public void run() {
360         cleaner.chore();
361       }
362     };
363     t.setDaemon(true);
364     t.start();
365     // wait until file clean started
366     while (cleaner.getNumOfDeletedSmallFiles() == 0) {
367       Thread.yield();
368     }
369 
370     // trigger configuration change
371     Configuration newConf = new Configuration(conf);
372     newConf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, UPDATE_THROTTLE_POINT);
373     newConf.setInt(HFileCleaner.LARGE_HFILE_QUEUE_INIT_SIZE, UPDATE_QUEUE_INIT_SIZE);
374     newConf.setInt(HFileCleaner.SMALL_HFILE_QUEUE_INIT_SIZE, UPDATE_QUEUE_INIT_SIZE);
375     newConf.setInt(HFileCleaner.LARGE_HFILE_DELETE_THREAD_NUMBER, LARGE_THREAD_NUM);
376     newConf.setInt(HFileCleaner.SMALL_HFILE_DELETE_THREAD_NUMBER, SMALL_THREAD_NUM);
377     newConf.setLong(HFileCleaner.HFILE_DELETE_THREAD_TIMEOUT_MSEC, THREAD_TIMEOUT_MSEC);
378     newConf.setLong(HFileCleaner.HFILE_DELETE_THREAD_CHECK_INTERVAL_MSEC,
379         THREAD_CHECK_INTERVAL_MSEC);
380     cleaner.onConfigurationChange(newConf);
381 
382     LOG.debug("File deleted from large queue: " + cleaner.getNumOfDeletedLargeFiles()
383         + "; from small queue: " + cleaner.getNumOfDeletedSmallFiles());
384     cleaner.onConfigurationChange(newConf);
385 
386     // check values after change
387     Assert.assertEquals(UPDATE_THROTTLE_POINT, cleaner.getThrottlePoint());
388     Assert.assertEquals(UPDATE_QUEUE_INIT_SIZE, cleaner.getLargeQueueInitSize());
389     Assert.assertEquals(UPDATE_QUEUE_INIT_SIZE, cleaner.getSmallQueueInitSize());
390     Assert.assertEquals(LARGE_THREAD_NUM + SMALL_THREAD_NUM, cleaner.getCleanerThreads().size());
391     Assert.assertEquals(THREAD_TIMEOUT_MSEC, cleaner.getCleanerThreadTimeoutMsec());
392     Assert.assertEquals(THREAD_CHECK_INTERVAL_MSEC, cleaner.getCleanerThreadCheckIntervalMsec());
393 
394     // make sure no cost when onConfigurationChange called with no change
395     List<Thread> oldThreads = cleaner.getCleanerThreads();
396     cleaner.onConfigurationChange(newConf);
397     List<Thread> newThreads = cleaner.getCleanerThreads();
398     Assert.assertArrayEquals(oldThreads.toArray(), newThreads.toArray());
399 
400     // wait until clean done and check
401     t.join();
402     LOG.debug("File deleted from large queue: " + cleaner.getNumOfDeletedLargeFiles()
403         + "; from small queue: " + cleaner.getNumOfDeletedSmallFiles());
404     Assert.assertTrue("Should delete more than " + LARGE_FILE_NUM
405         + " files from large queue but actually " + cleaner.getNumOfDeletedLargeFiles(),
406       cleaner.getNumOfDeletedLargeFiles() > LARGE_FILE_NUM);
407     Assert.assertTrue("Should delete less than " + SMALL_FILE_NUM
408         + " files from small queue but actually " + cleaner.getNumOfDeletedSmallFiles(),
409       cleaner.getNumOfDeletedSmallFiles() < SMALL_FILE_NUM);
410   }
411 
412   private void createFilesForTesting(int largeFileNum, int smallFileNum, FileSystem fs,
413       Path archivedHfileDir) throws IOException {
414     final Random rand = new Random();
415     final byte[] large = new byte[1024 * 1024];
416     for (int i = 0; i < large.length; i++) {
417       large[i] = (byte) rand.nextInt(128);
418     }
419     final byte[] small = new byte[1024];
420     for (int i = 0; i < small.length; i++) {
421       small[i] = (byte) rand.nextInt(128);
422     }
423     // create large and small files
424     for (int i = 1; i <= largeFileNum; i++) {
425       FSDataOutputStream out = fs.create(new Path(archivedHfileDir, "large-file-" + i));
426       out.write(large);
427       out.close();
428     }
429     for (int i = 1; i <= smallFileNum; i++) {
430       FSDataOutputStream out = fs.create(new Path(archivedHfileDir, "small-file-" + i));
431       out.write(small);
432       out.close();
433     }
434   }
435 }