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 com.google.common.base.Preconditions;
21  import com.google.common.collect.ImmutableSet;
22  import com.google.common.collect.Iterables;
23  import com.google.common.collect.Lists;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  import java.util.concurrent.atomic.AtomicInteger;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.fs.FileStatus;
36  import org.apache.hadoop.fs.FileSystem;
37  import org.apache.hadoop.fs.Path;
38  import org.apache.hadoop.fs.PathIsNotEmptyDirectoryException;
39  import org.apache.hadoop.hbase.ScheduledChore;
40  import org.apache.hadoop.hbase.Stoppable;
41  import org.apache.hadoop.hbase.classification.InterfaceAudience;
42  import org.apache.hadoop.ipc.RemoteException;
43  
44  /**
45   * Abstract Cleaner that uses a chain of delegates to clean a directory of files
46   * @param <T> Cleaner delegate class that is dynamically loaded from configuration
47   */
48  @InterfaceAudience.Private
49  public abstract class CleanerChore<T extends FileCleanerDelegate> extends ScheduledChore {
50  
51    private static final Log LOG = LogFactory.getLog(CleanerChore.class.getName());
52    private static final int AVAIL_PROCESSORS = Runtime.getRuntime().availableProcessors();
53  
54    /**
55     * If it is an integer and >= 1, it would be the size;
56     * if 0.0 < size <= 1.0, size would be available processors * size.
57     * Pay attention that 1.0 is different from 1, former indicates it will use 100% of cores,
58     * while latter will use only 1 thread for chore to scan dir.
59     */
60    public static final String CHORE_POOL_SIZE = "hbase.cleaner.scan.dir.concurrent.size";
61    static final String DEFAULT_CHORE_POOL_SIZE = "0.25";
62    private final DirScanPool pool;
63  
64    protected final FileSystem fs;
65    private final Path oldFileDir;
66    private final Configuration conf;
67    protected List<T> cleanersChain;
68    protected Map<String, Object> params;
69    private AtomicBoolean enabled = new AtomicBoolean(true);
70  
71    public CleanerChore(String name, final int sleepPeriod, final Stoppable s, Configuration conf,
72      FileSystem fs, Path oldFileDir, String confKey, DirScanPool pool) {
73      this(name, sleepPeriod, s, conf, fs, oldFileDir, confKey, pool, null);
74    }
75  
76    /**
77     * @param name name of the chore being run
78     * @param sleepPeriod the period of time to sleep between each run
79     * @param s the stopper
80     * @param conf configuration to use
81     * @param fs handle to the FS
82     * @param oldFileDir the path to the archived files
83     * @param confKey configuration key for the classes to instantiate
84     * @param pool the thread pool used to scan directories
85     * @param params members could be used in cleaner
86     */
87    public CleanerChore(String name, final int sleepPeriod, final Stoppable s,
88      Configuration conf, FileSystem fs, Path oldFileDir, String confKey,
89      DirScanPool pool, Map<String, Object> params) {
90      super(name, s, sleepPeriod);
91  
92      Preconditions.checkNotNull(pool, "Chore's pool can not be null");
93      this.pool = pool;
94      this.fs = fs;
95      this.oldFileDir = oldFileDir;
96      this.conf = conf;
97      this.params = params;
98      initCleanerChain(confKey);
99    }
100 
101   /**
102    * Calculate size for cleaner pool.
103    * @param poolSize size from configuration
104    * @return size of pool after calculation
105    */
106   static int calculatePoolSize(String poolSize) {
107     if (poolSize.matches("[1-9][0-9]*")) {
108       // If poolSize is an integer, return it directly,
109       // but upmost to the number of available processors.
110       int size = Math.min(Integer.parseInt(poolSize), AVAIL_PROCESSORS);
111       if (size == AVAIL_PROCESSORS) {
112         LOG.warn("Use full core processors to scan dir, size=" + size);
113       }
114       return size;
115     } else if (poolSize.matches("0.[0-9]+|1.0")) {
116       // if poolSize is a double, return poolSize * availableProcessors;
117       // Ensure that we always return at least one.
118       int computedThreads = (int) (AVAIL_PROCESSORS * Double.valueOf(poolSize));
119       if (computedThreads < 1) {
120         LOG.debug("Computed " + computedThreads + " threads for CleanerChore, using 1 instead");
121         return 1;
122       }
123       return computedThreads;
124     } else {
125       LOG.error("Unrecognized value: " + poolSize + " for " + CHORE_POOL_SIZE +
126           ", use default config: " + DEFAULT_CHORE_POOL_SIZE + " instead.");
127       return calculatePoolSize(DEFAULT_CHORE_POOL_SIZE);
128     }
129   }
130 
131   /**
132    * Validate the file to see if it even belongs in the directory. If it is valid, then the file
133    * will go through the cleaner delegates, but otherwise the file is just deleted.
134    * @param file full {@link Path} of the file to be checked
135    * @return <tt>true</tt> if the file is valid, <tt>false</tt> otherwise
136    */
137   protected abstract boolean validate(Path file);
138 
139   /**
140    * Instantiate and initialize all the file cleaners set in the configuration
141    * @param confKey key to get the file cleaner classes from the configuration
142    */
143   private void initCleanerChain(String confKey) {
144     this.cleanersChain = new LinkedList<T>();
145     String[] logCleaners = conf.getStrings(confKey);
146     if (logCleaners != null) {
147       for (String className : logCleaners) {
148         T logCleaner = newFileCleaner(className, conf);
149         if (logCleaner != null) {
150           LOG.info("initialize cleaner=" + className);
151           this.cleanersChain.add(logCleaner);
152         }
153       }
154     }
155   }
156 
157   /**
158    * A utility method to create new instances of LogCleanerDelegate based on the class name of the
159    * LogCleanerDelegate.
160    * @param className fully qualified class name of the LogCleanerDelegate
161    * @param conf
162    * @return the new instance
163    */
164   private T newFileCleaner(String className, Configuration conf) {
165     try {
166       Class<? extends FileCleanerDelegate> c = Class.forName(className).asSubclass(
167         FileCleanerDelegate.class);
168       @SuppressWarnings("unchecked")
169       T cleaner = (T) c.getDeclaredConstructor().newInstance();
170       cleaner.setConf(conf);
171       cleaner.init(this.params);
172       return cleaner;
173     } catch (Exception e) {
174       LOG.warn("Can NOT create CleanerDelegate: " + className, e);
175       // skipping if can't instantiate
176       return null;
177     }
178   }
179 
180   @Override
181   protected void chore() {
182     if (getEnabled()) {
183       try {
184         pool.latchCountUp();
185         if (runCleaner()) {
186           if (LOG.isTraceEnabled()) {
187             LOG.trace("Cleaned all WALs under " + oldFileDir);
188           }
189         } else {
190           if (LOG.isTraceEnabled()) {
191             LOG.trace("WALs outstanding under " + oldFileDir);
192           }
193         }
194       } finally {
195         pool.latchCountDown();
196       }
197       // This cleaner is waiting for other cleaners finishing their jobs.
198       // To avoid missing next chore, only wait 0.8 * period, then shutdown.
199       pool.tryUpdatePoolSize((long) (0.8 * getTimeUnit().toMillis(getPeriod())));
200     } else {
201       LOG.trace("Cleaner chore disabled! Not cleaning.");
202     }
203   }
204 
205   public boolean runCleaner() {
206     try {
207       final AsyncResult<Boolean> result = new AsyncResult<Boolean>();
208       pool.execute(new Runnable() {
209         @Override
210         public void run() {
211           traverseAndDelete(oldFileDir, true, result);
212         }
213       });
214       return result.get();
215     } catch (Exception e) {
216       LOG.info("Failed to traverse and delete paths under the dir: " + oldFileDir, e);
217       return false;
218     }
219   }
220 
221   /**
222    * Run the given files through each of the cleaners to see if it should be deleted, deleting it if
223    * necessary.
224    * @param files List of FileStatus for the files to check (and possibly delete)
225    * @return true iff successfully deleted all files
226    */
227   private boolean checkAndDeleteFiles(List<FileStatus> files) {
228     if (files == null) {
229       return true;
230     }
231 
232     // first check to see if the path is valid
233     List<FileStatus> validFiles = Lists.newArrayListWithCapacity(files.size());
234     List<FileStatus> invalidFiles = Lists.newArrayList();
235     for (FileStatus file : files) {
236       if (validate(file.getPath())) {
237         validFiles.add(file);
238       } else {
239         LOG.warn("Found a wrongly formatted file: " + file.getPath() + " - will delete it.");
240         invalidFiles.add(file);
241       }
242     }
243 
244     Iterable<FileStatus> deletableValidFiles = validFiles;
245     // check each of the cleaners for the valid files
246     for (T cleaner : cleanersChain) {
247       if (cleaner.isStopped() || getStopper().isStopped()) {
248         LOG.warn("A file cleaner" + this.getName() + " is stopped, won't delete any more files in:"
249             + this.oldFileDir);
250         return false;
251       }
252 
253       Iterable<FileStatus> filteredFiles = cleaner.getDeletableFiles(deletableValidFiles);
254 
255       // trace which cleaner is holding on to each file
256       if (LOG.isTraceEnabled()) {
257         ImmutableSet<FileStatus> filteredFileSet = ImmutableSet.copyOf(filteredFiles);
258         for (FileStatus file : deletableValidFiles) {
259           if (!filteredFileSet.contains(file)) {
260             LOG.trace(file.getPath() + " is not deletable according to:" + cleaner);
261           }
262         }
263       }
264 
265       deletableValidFiles = filteredFiles;
266     }
267 
268     Iterable<FileStatus> filesToDelete = Iterables.concat(invalidFiles, deletableValidFiles);
269     return deleteFiles(filesToDelete) == files.size();
270   }
271 
272   /**
273    * Delete the given files
274    * @param filesToDelete files to delete
275    * @return number of deleted files
276    */
277   protected int deleteFiles(Iterable<FileStatus> filesToDelete) {
278     int deletedFileCount = 0;
279     for (FileStatus file : filesToDelete) {
280       Path filePath = file.getPath();
281       LOG.trace("Removing " + file + " from archive");
282       try {
283         boolean success = this.fs.delete(filePath, false);
284         if (success) {
285           deletedFileCount++;
286         } else {
287           LOG.warn("Attempted to delete:" + filePath
288               + ", but couldn't. Run cleaner chain and attempt to delete on next pass.");
289         }
290       } catch (IOException e) {
291         e = e instanceof RemoteException ?
292             ((RemoteException)e).unwrapRemoteException() : e;
293         LOG.warn("Error while deleting: " + filePath, e);
294       }
295     }
296     return deletedFileCount;
297   }
298 
299   @Override
300   public synchronized void cleanup() {
301     for (T lc : this.cleanersChain) {
302       try {
303         lc.stop("Exiting");
304       } catch (Throwable t) {
305         LOG.warn("Stopping", t);
306       }
307     }
308   }
309 
310   int getChorePoolSize() {
311     return pool.getSize();
312   }
313 
314   /**
315    * @param enabled
316    */
317   public boolean setEnabled(final boolean enabled) {
318     return this.enabled.getAndSet(enabled);
319   }
320 
321   public boolean getEnabled() {
322     return this.enabled.get();
323   }
324 
325   private interface Action<T> {
326     T act() throws IOException;
327   }
328 
329   private interface Callback<T> {
330     void run(T val);
331   }
332 
333   private final class AsyncResult<T> {
334 
335     private Callback<T> callback;
336     private T result;
337     private boolean resultSet = false;
338 
339     AsyncResult(Callback<T> callback) {
340       this.callback = callback;
341     }
342 
343     AsyncResult() {
344     }
345 
346     void set(T result) {
347       synchronized (this) {
348         this.result = result;
349         if (callback != null) {
350           callback.run(result);
351         }
352         // Mark the result set process finished and notify the waiting get method.
353         this.resultSet = true;
354         this.notifyAll();
355       }
356     }
357 
358     synchronized T get() throws Exception {
359       while (!resultSet) {
360         wait();
361       }
362       return result;
363     }
364   }
365 
366   /**
367    * Attempts to clean up a directory(its subdirectories, and files) in a
368    * {@link java.util.concurrent.ThreadPoolExecutor} concurrently. We can get the final result by
369    * calling result.get().
370    * @param dir means the directory we will start to traverse and delete.
371    * @param root means whether it's the root directory to traverse, if true then cannot delete it.
372    * @param result {@link AsyncResult<Boolean>} to fetch the result. True means the current
373    *          directory has been deleted successfully (for root dir we don't need that) and the
374    *          parent will try to delete its own directory if all of the children(files and
375    *          sub-directories are included) has been deleted successfully.
376    */
377   private void traverseAndDelete(final Path dir, final boolean root,
378       final AsyncResult<Boolean> result) {
379     try {
380       final Action<Boolean> curDirDeletion = new Action<Boolean>() {
381         @Override
382         public Boolean act() throws IOException {
383           return fs.delete(dir, false);
384         }
385       };
386 
387       // Step.1: List all files under the given directory.
388       List<FileStatus> allPaths = Arrays.asList(fs.listStatus(dir));
389       final List<FileStatus> subDirs = new ArrayList<>();
390       final List<FileStatus> files = new ArrayList<>();
391       for (FileStatus status : allPaths) {
392         if (status.isDirectory()) {
393           subDirs.add(status);
394         } else if (status.isFile()) {
395           files.add(status);
396         }
397       }
398 
399       // Step.2: Try to delete all the deletable files.
400       final boolean allFilesDeleted = files.isEmpty() || deleteAction(new Action<Boolean>() {
401         @Override
402         public Boolean act() throws IOException {
403           return checkAndDeleteFiles(files);
404         }
405       }, "files", dir);
406 
407       // Step.3: Start to traverse and delete the sub-directories.
408       if (subDirs.isEmpty()) {
409         // If no sub-directories, then just try to delete the current dir and finish the result.
410         boolean deleted = allFilesDeleted;
411         if (allFilesDeleted && !root) {
412           deleted = deleteAction(curDirDeletion, "dir", dir);
413         }
414         result.set(deleted);
415         return;
416       }
417 
418       // Otherwise, there should be some sub-directories. then we will register the following
419       // callback in AsyncResult of sub-directory, and once all of the sub-directories are traversed
420       // and deleted then the callback will try to delete the current dir and finish the result.
421       final AtomicInteger remain = new AtomicInteger(subDirs.size());
422       Callback<Boolean> callback = new Callback<Boolean>() {
423         private volatile boolean allSubDirDeleted = true;
424 
425         @Override
426         public void run(Boolean subDirDeleted) {
427           allSubDirDeleted &= subDirDeleted;
428           if (remain.decrementAndGet() == 0) {
429             boolean deleted = allFilesDeleted && allSubDirDeleted;
430             if (deleted && !root) {
431               deleted = deleteAction(curDirDeletion, "dir", dir);
432             }
433             result.set(deleted);
434           }
435         }
436       };
437 
438       // Submit the request of sub-directory deletion.
439       for (FileStatus subDir : subDirs) {
440         final FileStatus finalSubDir = subDir;
441         // Register the callback in AsyncResult here.
442         final AsyncResult<Boolean> asyncResult = new AsyncResult<Boolean>(callback);
443         pool.execute(new Runnable() {
444           @Override
445           public void run() {
446             traverseAndDelete(finalSubDir.getPath(), false, asyncResult);
447           }
448         });
449       }
450     } catch (Exception e) {
451       result.set(false);
452       if (LOG.isDebugEnabled()) {
453         LOG.debug("Failed to traverse and delete the path=" + dir + ", root=" + root, e);
454       }
455     }
456   }
457 
458   /**
459    * Perform a delete on a specified type.
460    * @param deletion a delete
461    * @param type possible values are 'files', 'subdirs', 'dirs'
462    * @param dir delete actions happened under the given directory.
463    * @return true if it deleted successfully, false otherwise
464    */
465   private boolean deleteAction(Action<Boolean> deletion, String type, Path dir) {
466     boolean deleted;
467     try {
468       if (LOG.isTraceEnabled()) {
469         LOG.trace("Start deleting " + type + " under " + dir);
470       }
471       deleted = deletion.act();
472     } catch (PathIsNotEmptyDirectoryException exception) {
473       // N.B. HDFS throws this exception when we try to delete a non-empty directory, but
474       // LocalFileSystem throws a bare IOException. So some test code will get the verbose
475       // message below.
476       if (LOG.isDebugEnabled()) {
477         LOG.debug("Couldn't delete '" + dir + "' yet because it isn't empty w/exception.",
478           exception);
479       }
480       deleted = false;
481     } catch (IOException ioe) {
482       LOG.info(
483         "Could not delete " + type + " under " + dir + ". might be transient; we'll retry. if it "
484             + "keeps " + "happening, use following exception when asking on mailing list.",
485         ioe);
486       deleted = false;
487     } catch (Exception e) {
488       LOG.info("unexpected exception: ", e);
489       deleted = false;
490     }
491     if (LOG.isTraceEnabled()) {
492       LOG.trace("Finish deleting " + type + " under " + dir + ", deleted=" + deleted);
493     }
494     return deleted;
495   }
496 }