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}