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 java.io.IOException;
21  import java.util.Collections;
22  import java.util.LinkedList;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.concurrent.ExecutionException;
26  import java.util.concurrent.ForkJoinPool;
27  import java.util.concurrent.ForkJoinTask;
28  import java.util.concurrent.RecursiveTask;
29  import java.util.concurrent.atomic.AtomicBoolean;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.hadoop.conf.Configuration;
34  import org.apache.hadoop.fs.FileStatus;
35  import org.apache.hadoop.fs.FileSystem;
36  import org.apache.hadoop.fs.Path;
37  import org.apache.hadoop.fs.PathIsNotEmptyDirectoryException;
38  import org.apache.hadoop.hbase.ScheduledChore;
39  import org.apache.hadoop.hbase.Stoppable;
40  import org.apache.hadoop.hbase.classification.InterfaceAudience;
41  import org.apache.hadoop.hbase.conf.ConfigurationObserver;
42  import org.apache.hadoop.hbase.util.FSUtils;
43  import org.apache.hadoop.hbase.util.FileStatusFilter;
44  import org.apache.hadoop.ipc.RemoteException;
45  
46  import com.google.common.annotations.VisibleForTesting;
47  import com.google.common.base.Preconditions;
48  import com.google.common.collect.ImmutableSet;
49  import com.google.common.collect.Iterables;
50  import com.google.common.collect.Lists;
51  
52  /**
53   * Abstract Cleaner that uses a chain of delegates to clean a directory of files
54   * @param <T> Cleaner delegate class that is dynamically loaded from configuration
55   */
56  @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
57      justification="Static pool will be only updated once.")
58  @InterfaceAudience.Private
59  public abstract class CleanerChore<T extends FileCleanerDelegate> extends ScheduledChore
60      implements ConfigurationObserver {
61  
62    private static final Log LOG = LogFactory.getLog(CleanerChore.class.getName());
63    private static final int AVAIL_PROCESSORS = Runtime.getRuntime().availableProcessors();
64  
65    /**
66     * If it is an integer and >= 1, it would be the size;
67     * if 0.0 < size <= 1.0, size would be available processors * size.
68     * Pay attention that 1.0 is different from 1, former indicates it will use 100% of cores,
69     * while latter will use only 1 thread for chore to scan dir.
70     */
71    public static final String CHORE_POOL_SIZE = "hbase.cleaner.scan.dir.concurrent.size";
72    private static final String DEFAULT_CHORE_POOL_SIZE = "0.25";
73  
74    private static class DirScanPool {
75      int size;
76      ForkJoinPool pool;
77      int cleanerLatch;
78      AtomicBoolean reconfigNotification;
79  
80      DirScanPool(Configuration conf) {
81        String poolSize = conf.get(CHORE_POOL_SIZE, DEFAULT_CHORE_POOL_SIZE);
82        size = calculatePoolSize(poolSize);
83        // poolSize may be 0 or 0.0 from a careless configuration,
84        // double check to make sure.
85        size = size == 0 ? calculatePoolSize(DEFAULT_CHORE_POOL_SIZE) : size;
86        pool = new ForkJoinPool(size);
87        LOG.info("Cleaner pool size is " + size);
88        reconfigNotification = new AtomicBoolean(false);
89        cleanerLatch = 0;
90      }
91  
92      /**
93       * Checks if pool can be updated. If so, mark for update later.
94       * @param conf configuration
95       */
96      synchronized void markUpdate(Configuration conf) {
97        int newSize = calculatePoolSize(conf.get(CHORE_POOL_SIZE, DEFAULT_CHORE_POOL_SIZE));
98        if (newSize == size) {
99          LOG.trace("Size from configuration is same as previous=" + newSize +
100           " no need to update.");
101         return;
102       }
103       size = newSize;
104       // Chore is working, update it later.
105       reconfigNotification.set(true);
106     }
107 
108     /**
109      * Update pool with new size.
110      */
111     synchronized void updatePool(long timeout) {
112       long stopWaitTime = System.currentTimeMillis() + timeout;
113       while (cleanerLatch != 0 && timeout > 0) {
114         try {
115           wait(timeout);
116           timeout = stopWaitTime - System.currentTimeMillis();
117         } catch (InterruptedException ie) {
118           Thread.currentThread().interrupt();
119           break;
120         }
121       }
122       shutDownNow();
123       LOG.info("Update chore's pool size from " + pool.getParallelism() + " to " + size);
124       pool = new ForkJoinPool(size);
125     }
126 
127     synchronized void latchCountUp() {
128       cleanerLatch++;
129     }
130 
131     synchronized void latchCountDown() {
132       cleanerLatch--;
133       notifyAll();
134     }
135 
136     @SuppressWarnings({"FutureReturnValueIgnored","rawtypes","unchecked"})
137     synchronized void submit(ForkJoinTask task) {
138       pool.submit(task);
139     }
140 
141     synchronized void shutDownNow() {
142       if (pool == null || pool.isShutdown()) {
143         return;
144       }
145       pool.shutdownNow();
146     }
147   }
148   // It may be waste resources for each cleaner chore own its pool,
149   // so let's make pool for all cleaner chores.
150   private static volatile DirScanPool POOL;
151 
152   protected final FileSystem fs;
153   private final Path oldFileDir;
154   private final Configuration conf;
155   protected List<T> cleanersChain;
156   protected Map<String, Object> params;
157   private AtomicBoolean enabled = new AtomicBoolean(true);
158 
159   public static void initChorePool(Configuration conf) {
160     if (POOL == null) {
161       POOL = new DirScanPool(conf);
162     }
163   }
164 
165   public static void shutDownChorePool() {
166     if (POOL != null) {
167       POOL.shutDownNow();
168       POOL = null;
169     }
170   }
171 
172   public CleanerChore(String name, final int sleepPeriod, final Stoppable s, Configuration conf,
173                       FileSystem fs, Path oldFileDir, String confKey) {
174     this(name, sleepPeriod, s, conf, fs, oldFileDir, confKey, null);
175   }
176 
177   /**
178    * @param name name of the chore being run
179    * @param sleepPeriod the period of time to sleep between each run
180    * @param s the stopper
181    * @param conf configuration to use
182    * @param fs handle to the FS
183    * @param oldFileDir the path to the archived files
184    * @param confKey configuration key for the classes to instantiate
185    * @param params members could be used in cleaner
186    */
187   public CleanerChore(String name, final int sleepPeriod, final Stoppable s, Configuration conf,
188       FileSystem fs, Path oldFileDir, String confKey, Map<String, Object> params) {
189     super(name, s, sleepPeriod);
190 
191     Preconditions.checkNotNull(POOL, "Chore's pool isn't initialized, please call"
192         + "CleanerChore.initChorePool(Configuration) before new a cleaner chore.");
193     this.fs = fs;
194     this.oldFileDir = oldFileDir;
195     this.conf = conf;
196     this.params = params;
197     initCleanerChain(confKey);
198   }
199 
200 
201   /**
202    * Calculate size for cleaner pool.
203    * @param poolSize size from configuration
204    * @return size of pool after calculation
205    */
206   static int calculatePoolSize(String poolSize) {
207     if (poolSize.matches("[1-9][0-9]*")) {
208       // If poolSize is an integer, return it directly,
209       // but upmost to the number of available processors.
210       int size = Math.min(Integer.parseInt(poolSize), AVAIL_PROCESSORS);
211       if (size == AVAIL_PROCESSORS) {
212         LOG.warn("Use full core processors to scan dir, size=" + size);
213       }
214       return size;
215     } else if (poolSize.matches("0.[0-9]+|1.0")) {
216       // if poolSize is a double, return poolSize * availableProcessors;
217       // Ensure that we always return at least one.
218       int computedThreads = (int) (AVAIL_PROCESSORS * Double.valueOf(poolSize));
219       if (computedThreads < 1) {
220         LOG.debug("Computed " + computedThreads + " threads for CleanerChore, using 1 instead");
221         return 1;
222       }
223       return computedThreads;
224     } else {
225       LOG.error("Unrecognized value: " + poolSize + " for " + CHORE_POOL_SIZE +
226           ", use default config: " + DEFAULT_CHORE_POOL_SIZE + " instead.");
227       return calculatePoolSize(DEFAULT_CHORE_POOL_SIZE);
228     }
229   }
230 
231   /**
232    * Validate the file to see if it even belongs in the directory. If it is valid, then the file
233    * will go through the cleaner delegates, but otherwise the file is just deleted.
234    * @param file full {@link Path} of the file to be checked
235    * @return <tt>true</tt> if the file is valid, <tt>false</tt> otherwise
236    */
237   protected abstract boolean validate(Path file);
238 
239   /**
240    * Instantiate and initialize all the file cleaners set in the configuration
241    * @param confKey key to get the file cleaner classes from the configuration
242    */
243   private void initCleanerChain(String confKey) {
244     this.cleanersChain = new LinkedList<T>();
245     String[] logCleaners = conf.getStrings(confKey);
246     if (logCleaners != null) {
247       for (String className : logCleaners) {
248         T logCleaner = newFileCleaner(className, conf);
249         if (logCleaner != null) {
250           LOG.debug("initialize cleaner=" + className);
251           this.cleanersChain.add(logCleaner);
252         }
253       }
254     }
255   }
256 
257   @Override
258   public void onConfigurationChange(Configuration conf) {
259     POOL.markUpdate(conf);
260   }
261 
262   /**
263    * A utility method to create new instances of LogCleanerDelegate based on the class name of the
264    * LogCleanerDelegate.
265    * @param className fully qualified class name of the LogCleanerDelegate
266    * @param conf
267    * @return the new instance
268    */
269   private T newFileCleaner(String className, Configuration conf) {
270     try {
271       Class<? extends FileCleanerDelegate> c = Class.forName(className).asSubclass(
272         FileCleanerDelegate.class);
273       @SuppressWarnings("unchecked")
274       T cleaner = (T) c.newInstance();
275       cleaner.setConf(conf);
276       cleaner.init(this.params);
277       return cleaner;
278     } catch (Exception e) {
279       LOG.warn("Can NOT create CleanerDelegate: " + className, e);
280       // skipping if can't instantiate
281       return null;
282     }
283   }
284 
285   @Override
286   protected void chore() {
287     if (getEnabled()) {
288       try {
289         POOL.latchCountUp();
290         if (runCleaner()) {
291           if (LOG.isTraceEnabled()) {
292             LOG.trace("Cleaned all WALs under " + oldFileDir);
293           }
294         } else {
295           if (LOG.isTraceEnabled()) {
296             LOG.trace("WALs outstanding under " + oldFileDir);
297           }
298         }
299       } finally {
300         POOL.latchCountDown();
301       }
302       // After each cleaner chore, checks if received reconfigure notification while cleaning.
303       // First in cleaner turns off notification, to avoid another cleaner updating pool again.
304       if (POOL.reconfigNotification.compareAndSet(true, false)) {
305         // This cleaner is waiting for other cleaners finishing their jobs.
306         // To avoid missing next chore, only wait 0.8 * period, then shutdown.
307         POOL.updatePool((long) (0.8 * getTimeUnit().toMillis(getPeriod())));
308       }
309     } else {
310       LOG.trace("Cleaner chore disabled! Not cleaning.");
311     }
312   }
313 
314   public Boolean runCleaner() {
315     CleanerTask task = new CleanerTask(this.oldFileDir, true);
316     POOL.submit(task);
317     return task.join();
318   }
319 
320   /**
321    * Run the given files through each of the cleaners to see if it should be deleted, deleting it if
322    * necessary.
323    * @param files List of FileStatus for the files to check (and possibly delete)
324    * @return true iff successfully deleted all files
325    */
326   private boolean checkAndDeleteFiles(List<FileStatus> files) {
327     if (files == null) {
328       return true;
329     }
330 
331     // first check to see if the path is valid
332     List<FileStatus> validFiles = Lists.newArrayListWithCapacity(files.size());
333     List<FileStatus> invalidFiles = Lists.newArrayList();
334     for (FileStatus file : files) {
335       if (validate(file.getPath())) {
336         validFiles.add(file);
337       } else {
338         LOG.warn("Found a wrongly formatted file: " + file.getPath() + " - will delete it.");
339         invalidFiles.add(file);
340       }
341     }
342 
343     Iterable<FileStatus> deletableValidFiles = validFiles;
344     // check each of the cleaners for the valid files
345     for (T cleaner : cleanersChain) {
346       if (cleaner.isStopped() || getStopper().isStopped()) {
347         LOG.warn("A file cleaner" + this.getName() + " is stopped, won't delete any more files in:"
348             + this.oldFileDir);
349         return false;
350       }
351 
352       Iterable<FileStatus> filteredFiles = cleaner.getDeletableFiles(deletableValidFiles);
353       
354       // trace which cleaner is holding on to each file
355       if (LOG.isTraceEnabled()) {
356         ImmutableSet<FileStatus> filteredFileSet = ImmutableSet.copyOf(filteredFiles);
357         for (FileStatus file : deletableValidFiles) {
358           if (!filteredFileSet.contains(file)) {
359             LOG.trace(file.getPath() + " is not deletable according to:" + cleaner);
360           }
361         }
362       }
363       
364       deletableValidFiles = filteredFiles;
365     }
366     
367     Iterable<FileStatus> filesToDelete = Iterables.concat(invalidFiles, deletableValidFiles);
368     return deleteFiles(filesToDelete) == files.size();
369   }
370 
371   /**
372    * Delete the given files
373    * @param filesToDelete files to delete
374    * @return number of deleted files
375    */
376   protected int deleteFiles(Iterable<FileStatus> filesToDelete) {
377     int deletedFileCount = 0;
378     for (FileStatus file : filesToDelete) {
379       Path filePath = file.getPath();
380       LOG.trace("Removing " + file + " from archive");
381       try {
382         boolean success = this.fs.delete(filePath, false);
383         if (success) {
384           deletedFileCount++;
385         } else {
386           LOG.warn("Attempted to delete:" + filePath
387               + ", but couldn't. Run cleaner chain and attempt to delete on next pass.");
388         }
389       } catch (IOException e) {
390         e = e instanceof RemoteException ?
391             ((RemoteException)e).unwrapRemoteException() : e;
392         LOG.warn("Error while deleting: " + filePath, e);
393       }
394     }
395     return deletedFileCount;
396   }
397 
398   @Override
399   public void cleanup() {
400     for (T lc : this.cleanersChain) {
401       try {
402         lc.stop("Exiting");
403       } catch (Throwable t) {
404         LOG.warn("Stopping", t);
405       }
406     }
407   }
408 
409   @VisibleForTesting
410   int getChorePoolSize() {
411     return POOL.size;
412   }
413 
414   /**
415    * @param enabled
416    */
417   public boolean setEnabled(final boolean enabled) {
418     return this.enabled.getAndSet(enabled);
419   }
420 
421   public boolean getEnabled() {
422     return this.enabled.get();
423   }
424 
425   private interface Action<T> {
426     T act() throws IOException;
427   }
428 
429   /**
430    * Attemps to clean up a directory, its subdirectories, and files.
431    * Return value is true if everything was deleted. false on partial / total failures.
432    */
433   private class CleanerTask extends RecursiveTask<Boolean> {
434     private final Path dir;
435     private final boolean root;
436 
437     CleanerTask(final FileStatus dir, final boolean root) {
438       this(dir.getPath(), root);
439     }
440 
441     CleanerTask(final Path dir, final boolean root) {
442       this.dir = dir;
443       this.root = root;
444     }
445 
446     @Override
447     protected Boolean compute() {
448       LOG.trace("Cleaning under " + dir);
449       List<FileStatus> subDirs;
450       List<FileStatus> tmpFiles;
451       final List<FileStatus> files;
452       try {
453         // if dir doesn't exist, we'll get null back for both of these
454         // which will fall through to succeeding.
455         subDirs = FSUtils.listStatusWithStatusFilter(fs, dir, new FileStatusFilter() {
456           @Override
457           public boolean accept(FileStatus f) {
458             return f.isDirectory();
459           }
460         });
461         if (subDirs == null) {
462           subDirs = Collections.emptyList();
463         }
464         tmpFiles = FSUtils.listStatusWithStatusFilter(fs, dir, new FileStatusFilter() {
465           @Override
466           public boolean accept(FileStatus f) {
467             return f.isFile();
468           }
469         });
470         files = tmpFiles == null ? Collections.<FileStatus>emptyList() : tmpFiles;
471       } catch (IOException ioe) {
472         LOG.warn("failed to get FileStatus for contents of '" + dir + "'", ioe);
473         return false;
474       }
475 
476       boolean allFilesDeleted = true;
477       if (!files.isEmpty()) {
478         allFilesDeleted = deleteAction(new Action<Boolean>() {
479           @Override
480           public Boolean act() throws IOException {
481             return checkAndDeleteFiles(files);
482           }
483         }, "files");
484       }
485 
486       boolean allSubdirsDeleted = true;
487       if (!subDirs.isEmpty()) {
488         final List<CleanerTask> tasks = Lists.newArrayListWithCapacity(subDirs.size());
489         for (FileStatus subdir : subDirs) {
490           CleanerTask task = new CleanerTask(subdir, false);
491           tasks.add(task);
492           task.fork();
493         }
494         allSubdirsDeleted = deleteAction(new Action<Boolean>() {
495           @Override
496           public Boolean act() throws IOException {
497             return getCleanResult(tasks);
498           }
499         }, "subdirs");
500       }
501 
502       boolean result = allFilesDeleted && allSubdirsDeleted;
503       // if and only if files and subdirs under current dir are deleted successfully, and
504       // it is not the root dir, then task will try to delete it.
505       if (result && !root) {
506         result &= deleteAction(new Action<Boolean>() {
507           @Override
508           public Boolean act() throws IOException {
509             return fs.delete(dir, false);
510           }
511         }, "dir");
512       }
513       return result;
514     }
515 
516     /**
517      * Perform a delete on a specified type.
518      * @param deletion a delete
519      * @param type possible values are 'files', 'subdirs', 'dirs'
520      * @return true if it deleted successfully, false otherwise
521      */
522     private boolean deleteAction(Action<Boolean> deletion, String type) {
523       boolean deleted;
524       try {
525         LOG.trace("Start deleting " + type + " under " + dir);
526         deleted = deletion.act();
527       } catch (PathIsNotEmptyDirectoryException exception) {
528         // N.B. HDFS throws this exception when we try to delete a non-empty directory, but
529         // LocalFileSystem throws a bare IOException. So some test code will get the verbose
530         // message below.
531         LOG.debug("Couldn't delete '" + dir + "' yet because it isn't empty. Probably transient. " +
532             "exception details at TRACE.");
533         LOG.trace("Couldn't delete '" + dir + "' yet because it isn't empty w/exception.",
534             exception);
535         deleted = false;
536       } catch (IOException ioe) {
537         LOG.info("Could not delete " + type + " under " + dir + ". might be transient; we'll " +
538             "retry. if it keeps happening, use following exception when asking on mailing list.",
539             ioe);
540         deleted = false;
541       }
542       LOG.trace("Finish deleting " + type + " under " + dir + " deleted=" + deleted);
543       return deleted;
544     }
545 
546     /**
547      * Get cleaner results of subdirs.
548      * @param tasks subdirs cleaner tasks
549      * @return true if all subdirs deleted successfully, false for patial/all failures
550      * @throws IOException something happen during computation
551      */
552     private boolean getCleanResult(List<CleanerTask> tasks) throws IOException {
553       boolean cleaned = true;
554       try {
555         for (CleanerTask task : tasks) {
556           cleaned &= task.get();
557         }
558       } catch (InterruptedException | ExecutionException e) {
559         throw new IOException(e);
560       }
561       return cleaned;
562     }
563   }
564 
565 }