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.List; 026import java.util.Random; 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.Path; 032import org.apache.hadoop.hbase.ChoreService; 033import org.apache.hadoop.hbase.CoordinatedStateManager; 034import org.apache.hadoop.hbase.HBaseClassTestRule; 035import org.apache.hadoop.hbase.HBaseTestingUtility; 036import org.apache.hadoop.hbase.HConstants; 037import org.apache.hadoop.hbase.Server; 038import org.apache.hadoop.hbase.ServerName; 039import org.apache.hadoop.hbase.client.ClusterConnection; 040import org.apache.hadoop.hbase.client.Connection; 041import org.apache.hadoop.hbase.testclassification.MasterTests; 042import org.apache.hadoop.hbase.testclassification.MediumTests; 043import org.apache.hadoop.hbase.util.EnvironmentEdge; 044import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; 045import org.apache.hadoop.hbase.zookeeper.MetaTableLocator; 046import org.apache.hadoop.hbase.zookeeper.ZKWatcher; 047import org.junit.AfterClass; 048import org.junit.Assert; 049import org.junit.BeforeClass; 050import org.junit.ClassRule; 051import org.junit.Test; 052import org.junit.experimental.categories.Category; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056@Category({MasterTests.class, MediumTests.class}) 057public class TestHFileCleaner { 058 059 @ClassRule 060 public static final HBaseClassTestRule CLASS_RULE = 061 HBaseClassTestRule.forClass(TestHFileCleaner.class); 062 063 private static final Logger LOG = LoggerFactory.getLogger(TestHFileCleaner.class); 064 065 private final static HBaseTestingUtility UTIL = new HBaseTestingUtility(); 066 067 @BeforeClass 068 public static void setupCluster() throws Exception { 069 // have to use a minidfs cluster because the localfs doesn't modify file times correctly 070 UTIL.startMiniDFSCluster(1); 071 } 072 073 @AfterClass 074 public static void shutdownCluster() throws IOException { 075 UTIL.shutdownMiniDFSCluster(); 076 } 077 078 @Test 079 public void testTTLCleaner() throws IOException, InterruptedException { 080 FileSystem fs = UTIL.getDFSCluster().getFileSystem(); 081 Path root = UTIL.getDataTestDirOnTestFS(); 082 Path file = new Path(root, "file"); 083 fs.createNewFile(file); 084 long createTime = System.currentTimeMillis(); 085 assertTrue("Test file not created!", fs.exists(file)); 086 TimeToLiveHFileCleaner cleaner = new TimeToLiveHFileCleaner(); 087 // update the time info for the file, so the cleaner removes it 088 fs.setTimes(file, createTime - 100, -1); 089 Configuration conf = UTIL.getConfiguration(); 090 conf.setLong(TimeToLiveHFileCleaner.TTL_CONF_KEY, 100); 091 cleaner.setConf(conf); 092 assertTrue("File not set deletable - check mod time:" + getFileStats(file, fs) 093 + " with create time:" + createTime, cleaner.isFileDeletable(fs.getFileStatus(file))); 094 } 095 096 /** 097 * @param file to check 098 * @return loggable information about the file 099 */ 100 private String getFileStats(Path file, FileSystem fs) throws IOException { 101 FileStatus status = fs.getFileStatus(file); 102 return "File" + file + ", mtime:" + status.getModificationTime() + ", atime:" 103 + status.getAccessTime(); 104 } 105 106 @Test 107 public void testHFileCleaning() throws Exception { 108 final EnvironmentEdge originalEdge = EnvironmentEdgeManager.getDelegate(); 109 String prefix = "someHFileThatWouldBeAUUID"; 110 Configuration conf = UTIL.getConfiguration(); 111 // set TTL 112 long ttl = 2000; 113 conf.set(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, 114 "org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner"); 115 conf.setLong(TimeToLiveHFileCleaner.TTL_CONF_KEY, ttl); 116 Server server = new DummyServer(); 117 Path archivedHfileDir = new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY); 118 FileSystem fs = FileSystem.get(conf); 119 HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir); 120 121 // Create 2 invalid files, 1 "recent" file, 1 very new file and 30 old files 122 final long createTime = System.currentTimeMillis(); 123 fs.delete(archivedHfileDir, true); 124 fs.mkdirs(archivedHfileDir); 125 // Case 1: 1 invalid file, which should be deleted directly 126 fs.createNewFile(new Path(archivedHfileDir, "dfd-dfd")); 127 // Case 2: 1 "recent" file, not even deletable for the first log cleaner 128 // (TimeToLiveLogCleaner), so we are not going down the chain 129 LOG.debug("Now is: " + createTime); 130 for (int i = 1; i < 32; i++) { 131 // Case 3: old files which would be deletable for the first log cleaner 132 // (TimeToLiveHFileCleaner), 133 Path fileName = new Path(archivedHfileDir, (prefix + "." + (createTime + i))); 134 fs.createNewFile(fileName); 135 // set the creation time past ttl to ensure that it gets removed 136 fs.setTimes(fileName, createTime - ttl - 1, -1); 137 LOG.debug("Creating " + getFileStats(fileName, fs)); 138 } 139 140 // Case 2: 1 newer file, not even deletable for the first log cleaner 141 // (TimeToLiveLogCleaner), so we are not going down the chain 142 Path saved = new Path(archivedHfileDir, prefix + ".00000000000"); 143 fs.createNewFile(saved); 144 // set creation time within the ttl 145 fs.setTimes(saved, createTime - ttl / 2, -1); 146 LOG.debug("Creating " + getFileStats(saved, fs)); 147 for (FileStatus stat : fs.listStatus(archivedHfileDir)) { 148 LOG.debug(stat.getPath().toString()); 149 } 150 151 assertEquals(33, fs.listStatus(archivedHfileDir).length); 152 153 // set a custom edge manager to handle time checking 154 EnvironmentEdge setTime = new EnvironmentEdge() { 155 @Override 156 public long currentTime() { 157 return createTime; 158 } 159 }; 160 EnvironmentEdgeManager.injectEdge(setTime); 161 162 // run the chore 163 cleaner.chore(); 164 165 // ensure we only end up with the saved file 166 assertEquals(1, fs.listStatus(archivedHfileDir).length); 167 168 for (FileStatus file : fs.listStatus(archivedHfileDir)) { 169 LOG.debug("Kept hfiles: " + file.getPath().getName()); 170 } 171 172 // reset the edge back to the original edge 173 EnvironmentEdgeManager.injectEdge(originalEdge); 174 } 175 176 @Test 177 public void testRemovesEmptyDirectories() throws Exception { 178 Configuration conf = UTIL.getConfiguration(); 179 // no cleaner policies = delete all files 180 conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, ""); 181 Server server = new DummyServer(); 182 Path archivedHfileDir = new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY); 183 184 // setup the cleaner 185 FileSystem fs = UTIL.getDFSCluster().getFileSystem(); 186 HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir); 187 188 // make all the directories for archiving files 189 Path table = new Path(archivedHfileDir, "table"); 190 Path region = new Path(table, "regionsomthing"); 191 Path family = new Path(region, "fam"); 192 Path file = new Path(family, "file12345"); 193 fs.mkdirs(family); 194 if (!fs.exists(family)) throw new RuntimeException("Couldn't create test family:" + family); 195 fs.create(file).close(); 196 if (!fs.exists(file)) throw new RuntimeException("Test file didn't get created:" + file); 197 198 // run the chore to cleanup the files (and the directories above it) 199 cleaner.chore(); 200 201 // make sure all the parent directories get removed 202 assertFalse("family directory not removed for empty directory", fs.exists(family)); 203 assertFalse("region directory not removed for empty directory", fs.exists(region)); 204 assertFalse("table directory not removed for empty directory", fs.exists(table)); 205 assertTrue("archive directory", fs.exists(archivedHfileDir)); 206 } 207 208 static class DummyServer implements Server { 209 @Override 210 public Configuration getConfiguration() { 211 return UTIL.getConfiguration(); 212 } 213 214 @Override 215 public ZKWatcher getZooKeeper() { 216 try { 217 return new ZKWatcher(getConfiguration(), "dummy server", this); 218 } catch (IOException e) { 219 e.printStackTrace(); 220 } 221 return null; 222 } 223 224 @Override 225 public CoordinatedStateManager getCoordinatedStateManager() { 226 return null; 227 } 228 229 @Override 230 public ClusterConnection getConnection() { 231 return null; 232 } 233 234 @Override 235 public MetaTableLocator getMetaTableLocator() { 236 return null; 237 } 238 239 @Override 240 public ServerName getServerName() { 241 return ServerName.valueOf("regionserver,60020,000000"); 242 } 243 244 @Override 245 public void abort(String why, Throwable e) { 246 } 247 248 @Override 249 public boolean isAborted() { 250 return false; 251 } 252 253 @Override 254 public void stop(String why) { 255 } 256 257 @Override 258 public boolean isStopped() { 259 return false; 260 } 261 262 @Override 263 public ChoreService getChoreService() { 264 return null; 265 } 266 267 @Override 268 public ClusterConnection getClusterConnection() { 269 // TODO Auto-generated method stub 270 return null; 271 } 272 273 @Override 274 public FileSystem getFileSystem() { 275 return null; 276 } 277 278 @Override 279 public boolean isStopping() { 280 return false; 281 } 282 283 @Override 284 public Connection createConnection(Configuration conf) throws IOException { 285 return null; 286 } 287 } 288 289 @Test 290 public void testThreadCleanup() throws Exception { 291 Configuration conf = UTIL.getConfiguration(); 292 conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, ""); 293 Server server = new DummyServer(); 294 Path archivedHfileDir = 295 new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY); 296 297 // setup the cleaner 298 FileSystem fs = UTIL.getDFSCluster().getFileSystem(); 299 HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir); 300 // clean up archive directory 301 fs.delete(archivedHfileDir, true); 302 fs.mkdirs(archivedHfileDir); 303 // create some file to delete 304 fs.createNewFile(new Path(archivedHfileDir, "dfd-dfd")); 305 // launch the chore 306 cleaner.chore(); 307 // call cleanup 308 cleaner.cleanup(); 309 // wait awhile for thread to die 310 Thread.sleep(100); 311 for (Thread thread : cleaner.getCleanerThreads()) { 312 Assert.assertFalse(thread.isAlive()); 313 } 314 } 315 316 @Test 317 public void testLargeSmallIsolation() throws Exception { 318 Configuration conf = UTIL.getConfiguration(); 319 // no cleaner policies = delete all files 320 conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, ""); 321 conf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, 512 * 1024); 322 Server server = new DummyServer(); 323 Path archivedHfileDir = 324 new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY); 325 326 // setup the cleaner 327 FileSystem fs = UTIL.getDFSCluster().getFileSystem(); 328 HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir); 329 // clean up archive directory 330 fs.delete(archivedHfileDir, true); 331 fs.mkdirs(archivedHfileDir); 332 // necessary set up 333 final int LARGE_FILE_NUM = 5; 334 final int SMALL_FILE_NUM = 20; 335 createFilesForTesting(LARGE_FILE_NUM, SMALL_FILE_NUM, fs, archivedHfileDir); 336 // call cleanup 337 cleaner.chore(); 338 339 Assert.assertEquals(LARGE_FILE_NUM, cleaner.getNumOfDeletedLargeFiles()); 340 Assert.assertEquals(SMALL_FILE_NUM, cleaner.getNumOfDeletedSmallFiles()); 341 } 342 343 @Test 344 public void testOnConfigurationChange() throws Exception { 345 // constants 346 final int ORIGINAL_THROTTLE_POINT = 512 * 1024; 347 final int ORIGINAL_QUEUE_INIT_SIZE = 512; 348 final int UPDATE_THROTTLE_POINT = 1024;// small enough to change large/small check 349 final int UPDATE_QUEUE_INIT_SIZE = 1024; 350 final int LARGE_FILE_NUM = 5; 351 final int SMALL_FILE_NUM = 20; 352 final int LARGE_THREAD_NUM = 2; 353 final int SMALL_THREAD_NUM = 4; 354 final long THREAD_TIMEOUT_MSEC = 30 * 1000L; 355 final long THREAD_CHECK_INTERVAL_MSEC = 500L; 356 357 Configuration conf = UTIL.getConfiguration(); 358 // no cleaner policies = delete all files 359 conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, ""); 360 conf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, ORIGINAL_THROTTLE_POINT); 361 conf.setInt(HFileCleaner.LARGE_HFILE_QUEUE_INIT_SIZE, ORIGINAL_QUEUE_INIT_SIZE); 362 conf.setInt(HFileCleaner.SMALL_HFILE_QUEUE_INIT_SIZE, ORIGINAL_QUEUE_INIT_SIZE); 363 Server server = new DummyServer(); 364 Path archivedHfileDir = 365 new Path(UTIL.getDataTestDirOnTestFS(), HConstants.HFILE_ARCHIVE_DIRECTORY); 366 367 // setup the cleaner 368 FileSystem fs = UTIL.getDFSCluster().getFileSystem(); 369 final HFileCleaner cleaner = new HFileCleaner(1000, server, conf, fs, archivedHfileDir); 370 Assert.assertEquals(ORIGINAL_THROTTLE_POINT, cleaner.getThrottlePoint()); 371 Assert.assertEquals(ORIGINAL_QUEUE_INIT_SIZE, cleaner.getLargeQueueInitSize()); 372 Assert.assertEquals(ORIGINAL_QUEUE_INIT_SIZE, cleaner.getSmallQueueInitSize()); 373 Assert.assertEquals(HFileCleaner.DEFAULT_HFILE_DELETE_THREAD_TIMEOUT_MSEC, 374 cleaner.getCleanerThreadTimeoutMsec()); 375 Assert.assertEquals(HFileCleaner.DEFAULT_HFILE_DELETE_THREAD_CHECK_INTERVAL_MSEC, 376 cleaner.getCleanerThreadCheckIntervalMsec()); 377 378 // clean up archive directory and create files for testing 379 fs.delete(archivedHfileDir, true); 380 fs.mkdirs(archivedHfileDir); 381 createFilesForTesting(LARGE_FILE_NUM, SMALL_FILE_NUM, fs, archivedHfileDir); 382 383 // call cleaner, run as daemon to test the interrupt-at-middle case 384 Thread t = new Thread() { 385 @Override 386 public void run() { 387 cleaner.chore(); 388 } 389 }; 390 t.setDaemon(true); 391 t.start(); 392 // wait until file clean started 393 while (cleaner.getNumOfDeletedSmallFiles() == 0) { 394 Thread.yield(); 395 } 396 397 // trigger configuration change 398 Configuration newConf = new Configuration(conf); 399 newConf.setInt(HFileCleaner.HFILE_DELETE_THROTTLE_THRESHOLD, UPDATE_THROTTLE_POINT); 400 newConf.setInt(HFileCleaner.LARGE_HFILE_QUEUE_INIT_SIZE, UPDATE_QUEUE_INIT_SIZE); 401 newConf.setInt(HFileCleaner.SMALL_HFILE_QUEUE_INIT_SIZE, UPDATE_QUEUE_INIT_SIZE); 402 newConf.setInt(HFileCleaner.LARGE_HFILE_DELETE_THREAD_NUMBER, LARGE_THREAD_NUM); 403 newConf.setInt(HFileCleaner.SMALL_HFILE_DELETE_THREAD_NUMBER, SMALL_THREAD_NUM); 404 newConf.setLong(HFileCleaner.HFILE_DELETE_THREAD_TIMEOUT_MSEC, THREAD_TIMEOUT_MSEC); 405 newConf.setLong(HFileCleaner.HFILE_DELETE_THREAD_CHECK_INTERVAL_MSEC, 406 THREAD_CHECK_INTERVAL_MSEC); 407 408 LOG.debug("File deleted from large queue: " + cleaner.getNumOfDeletedLargeFiles() 409 + "; from small queue: " + cleaner.getNumOfDeletedSmallFiles()); 410 cleaner.onConfigurationChange(newConf); 411 412 // check values after change 413 Assert.assertEquals(UPDATE_THROTTLE_POINT, cleaner.getThrottlePoint()); 414 Assert.assertEquals(UPDATE_QUEUE_INIT_SIZE, cleaner.getLargeQueueInitSize()); 415 Assert.assertEquals(UPDATE_QUEUE_INIT_SIZE, cleaner.getSmallQueueInitSize()); 416 Assert.assertEquals(LARGE_THREAD_NUM + SMALL_THREAD_NUM, cleaner.getCleanerThreads().size()); 417 Assert.assertEquals(THREAD_TIMEOUT_MSEC, cleaner.getCleanerThreadTimeoutMsec()); 418 Assert.assertEquals(THREAD_CHECK_INTERVAL_MSEC, cleaner.getCleanerThreadCheckIntervalMsec()); 419 420 // make sure no cost when onConfigurationChange called with no change 421 List<Thread> oldThreads = cleaner.getCleanerThreads(); 422 cleaner.onConfigurationChange(newConf); 423 List<Thread> newThreads = cleaner.getCleanerThreads(); 424 Assert.assertArrayEquals(oldThreads.toArray(), newThreads.toArray()); 425 426 // wait until clean done and check 427 t.join(); 428 LOG.debug("File deleted from large queue: " + cleaner.getNumOfDeletedLargeFiles() 429 + "; from small queue: " + cleaner.getNumOfDeletedSmallFiles()); 430 Assert.assertTrue("Should delete more than " + LARGE_FILE_NUM 431 + " files from large queue but actually " + cleaner.getNumOfDeletedLargeFiles(), 432 cleaner.getNumOfDeletedLargeFiles() > LARGE_FILE_NUM); 433 Assert.assertTrue("Should delete less than " + SMALL_FILE_NUM 434 + " files from small queue but actually " + cleaner.getNumOfDeletedSmallFiles(), 435 cleaner.getNumOfDeletedSmallFiles() < SMALL_FILE_NUM); 436 } 437 438 private void createFilesForTesting(int largeFileNum, int smallFileNum, FileSystem fs, 439 Path archivedHfileDir) throws IOException { 440 final Random rand = new Random(); 441 final byte[] large = new byte[1024 * 1024]; 442 for (int i = 0; i < large.length; i++) { 443 large[i] = (byte) rand.nextInt(128); 444 } 445 final byte[] small = new byte[1024]; 446 for (int i = 0; i < small.length; i++) { 447 small[i] = (byte) rand.nextInt(128); 448 } 449 // create large and small files 450 for (int i = 1; i <= largeFileNum; i++) { 451 FSDataOutputStream out = fs.create(new Path(archivedHfileDir, "large-file-" + i)); 452 out.write(large); 453 out.close(); 454 } 455 for (int i = 1; i <= smallFileNum; i++) { 456 FSDataOutputStream out = fs.create(new Path(archivedHfileDir, "small-file-" + i)); 457 out.write(small); 458 out.close(); 459 } 460 } 461}