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