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")
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    * Calculate size for cleaner pool.
202    * @param poolSize size from configuration
203    * @return size of pool after calculation
204    */
205   static int calculatePoolSize(String poolSize) {
206     if (poolSize.matches("[1-9][0-9]*")) {
207       // If poolSize is an integer, return it directly,
208       // but upmost to the number of available processors.
209       int size = Math.min(Integer.parseInt(poolSize), AVAIL_PROCESSORS);
210       if (size == AVAIL_PROCESSORS) {
211         LOG.warn("Use full core processors to scan dir, size=" + size);
212       }
213       return size;
214     } else if (poolSize.matches("0.[0-9]+|1.0")) {
215       // if poolSize is a double, return poolSize * availableProcessors;
216       // Ensure that we always return at least one.
217       int computedThreads = (int) (AVAIL_PROCESSORS * Double.valueOf(poolSize));
218       if (computedThreads < 1) {
219         LOG.debug("Computed " + computedThreads + " threads for CleanerChore, using 1 instead");
220         return 1;
221       }
222       return computedThreads;
223     } else {
224       LOG.error("Unrecognized value: " + poolSize + " for " + CHORE_POOL_SIZE +
225           ", use default config: " + DEFAULT_CHORE_POOL_SIZE + " instead.");
226       return calculatePoolSize(DEFAULT_CHORE_POOL_SIZE);
227     }
228   }
229 
230   /**
231    * Validate the file to see if it even belongs in the directory. If it is valid, then the file
232    * will go through the cleaner delegates, but otherwise the file is just deleted.
233    * @param file full {@link Path} of the file to be checked
234    * @return <tt>true</tt> if the file is valid, <tt>false</tt> otherwise
235    */
236   protected abstract boolean validate(Path file);
237 
238   /**
239    * Instantiate and initialize all the file cleaners set in the configuration
240    * @param confKey key to get the file cleaner classes from the configuration
241    */
242   private void initCleanerChain(String confKey) {
243     this.cleanersChain = new LinkedList<T>();
244     String[] logCleaners = conf.getStrings(confKey);
245     if (logCleaners != null) {
246       for (String className : logCleaners) {
247         T logCleaner = newFileCleaner(className, conf);
248         if (logCleaner != null) {
249           LOG.debug("initialize cleaner=" + className);
250           this.cleanersChain.add(logCleaner);
251         }
252       }
253     }
254   }
255 
256   @Override
257   public void onConfigurationChange(Configuration conf) {
258     POOL.markUpdate(conf);
259   }
260 
261   /**
262    * A utility method to create new instances of LogCleanerDelegate based on the class name of the
263    * LogCleanerDelegate.
264    * @param className fully qualified class name of the LogCleanerDelegate
265    * @param conf
266    * @return the new instance
267    */
268   private T newFileCleaner(String className, Configuration conf) {
269     try {
270       Class<? extends FileCleanerDelegate> c = Class.forName(className).asSubclass(
271         FileCleanerDelegate.class);
272       @SuppressWarnings("unchecked")
273       T cleaner = (T) c.getDeclaredConstructor().newInstance();
274       cleaner.setConf(conf);
275       cleaner.init(this.params);
276       return cleaner;
277     } catch (Exception e) {
278       LOG.warn("Can NOT create CleanerDelegate: " + className, e);
279       // skipping if can't instantiate
280       return null;
281     }
282   }
283 
284   @Override
285   protected void chore() {
286     if (getEnabled()) {
287       try {
288         POOL.latchCountUp();
289         if (runCleaner()) {
290           if (LOG.isTraceEnabled()) {
291             LOG.trace("Cleaned all WALs under " + oldFileDir);
292           }
293         } else {
294           if (LOG.isTraceEnabled()) {
295             LOG.trace("WALs outstanding under " + oldFileDir);
296           }
297         }
298       } finally {
299         POOL.latchCountDown();
300       }
301       // After each cleaner chore, checks if received reconfigure notification while cleaning.
302       // First in cleaner turns off notification, to avoid another cleaner updating pool again.
303       if (POOL.reconfigNotification.compareAndSet(true, false)) {
304         // This cleaner is waiting for other cleaners finishing their jobs.
305         // To avoid missing next chore, only wait 0.8 * period, then shutdown.
306         POOL.updatePool((long) (0.8 * getTimeUnit().toMillis(getPeriod())));
307       }
308     } else {
309       LOG.trace("Cleaner chore disabled! Not cleaning.");
310     }
311   }
312 
313   public Boolean runCleaner() {
314     CleanerTask task = new CleanerTask(this.oldFileDir, true);
315     POOL.submit(task);
316     return task.join();
317   }
318 
319   /**
320    * Run the given files through each of the cleaners to see if it should be deleted, deleting it if
321    * necessary.
322    * @param files List of FileStatus for the files to check (and possibly delete)
323    * @return true iff successfully deleted all files
324    */
325   private boolean checkAndDeleteFiles(List<FileStatus> files) {
326     if (files == null) {
327       return true;
328     }
329 
330     // first check to see if the path is valid
331     List<FileStatus> validFiles = Lists.newArrayListWithCapacity(files.size());
332     List<FileStatus> invalidFiles = Lists.newArrayList();
333     for (FileStatus file : files) {
334       if (validate(file.getPath())) {
335         validFiles.add(file);
336       } else {
337         LOG.warn("Found a wrongly formatted file: " + file.getPath() + " - will delete it.");
338         invalidFiles.add(file);
339       }
340     }
341 
342     Iterable<FileStatus> deletableValidFiles = validFiles;
343     // check each of the cleaners for the valid files
344     for (T cleaner : cleanersChain) {
345       if (cleaner.isStopped() || getStopper().isStopped()) {
346         LOG.warn("A file cleaner" + this.getName() + " is stopped, won't delete any more files in:"
347             + this.oldFileDir);
348         return false;
349       }
350 
351       Iterable<FileStatus> filteredFiles = cleaner.getDeletableFiles(deletableValidFiles);
352       
353       // trace which cleaner is holding on to each file
354       if (LOG.isTraceEnabled()) {
355         ImmutableSet<FileStatus> filteredFileSet = ImmutableSet.copyOf(filteredFiles);
356         for (FileStatus file : deletableValidFiles) {
357           if (!filteredFileSet.contains(file)) {
358             LOG.trace(file.getPath() + " is not deletable according to:" + cleaner);
359           }
360         }
361       }
362       
363       deletableValidFiles = filteredFiles;
364     }
365     
366     Iterable<FileStatus> filesToDelete = Iterables.concat(invalidFiles, deletableValidFiles);
367     return deleteFiles(filesToDelete) == files.size();
368   }
369 
370   /**
371    * Delete the given files
372    * @param filesToDelete files to delete
373    * @return number of deleted files
374    */
375   protected int deleteFiles(Iterable<FileStatus> filesToDelete) {
376     int deletedFileCount = 0;
377     for (FileStatus file : filesToDelete) {
378       Path filePath = file.getPath();
379       LOG.trace("Removing " + file + " from archive");
380       try {
381         boolean success = this.fs.delete(filePath, false);
382         if (success) {
383           deletedFileCount++;
384         } else {
385           LOG.warn("Attempted to delete:" + filePath
386               + ", but couldn't. Run cleaner chain and attempt to delete on next pass.");
387         }
388       } catch (IOException e) {
389         e = e instanceof RemoteException ?
390             ((RemoteException)e).unwrapRemoteException() : e;
391         LOG.warn("Error while deleting: " + filePath, e);
392       }
393     }
394     return deletedFileCount;
395   }
396 
397   @Override
398   public synchronized void cleanup() {
399     for (T lc : this.cleanersChain) {
400       try {
401         lc.stop("Exiting");
402       } catch (Throwable t) {
403         LOG.warn("Stopping", t);
404       }
405     }
406   }
407 
408   @VisibleForTesting
409   int getChorePoolSize() {
410     return POOL.size;
411   }
412 
413   /**
414    * @param enabled
415    */
416   public boolean setEnabled(final boolean enabled) {
417     return this.enabled.getAndSet(enabled);
418   }
419 
420   public boolean getEnabled() {
421     return this.enabled.get();
422   }
423 
424   private interface Action<T> {
425     T act() throws IOException;
426   }
427 
428   /**
429    * Attemps to clean up a directory, its subdirectories, and files.
430    * Return value is true if everything was deleted. false on partial / total failures.
431    */
432   private class CleanerTask extends RecursiveTask<Boolean> {
433     private final Path dir;
434     private final boolean root;
435 
436     CleanerTask(final FileStatus dir, final boolean root) {
437       this(dir.getPath(), root);
438     }
439 
440     CleanerTask(final Path dir, final boolean root) {
441       this.dir = dir;
442       this.root = root;
443     }
444 
445     @Override
446     protected Boolean compute() {
447       LOG.trace("Cleaning under " + dir);
448       List<FileStatus> subDirs;
449       List<FileStatus> tmpFiles;
450       final List<FileStatus> files;
451       try {
452         // if dir doesn't exist, we'll get null back for both of these
453         // which will fall through to succeeding.
454         subDirs = FSUtils.listStatusWithStatusFilter(fs, dir, new FileStatusFilter() {
455           @Override
456           public boolean accept(FileStatus f) {
457             return f.isDirectory();
458           }
459         });
460         if (subDirs == null) {
461           subDirs = Collections.emptyList();
462         }
463         tmpFiles = FSUtils.listStatusWithStatusFilter(fs, dir, new FileStatusFilter() {
464           @Override
465           public boolean accept(FileStatus f) {
466             return f.isFile();
467           }
468         });
469         files = tmpFiles == null ? Collections.<FileStatus>emptyList() : tmpFiles;
470       } catch (IOException ioe) {
471         LOG.warn("failed to get FileStatus for contents of '" + dir + "'", ioe);
472         return false;
473       }
474 
475       boolean allFilesDeleted = true;
476       if (!files.isEmpty()) {
477         allFilesDeleted = deleteAction(new Action<Boolean>() {
478           @Override
479           public Boolean act() throws IOException {
480             return checkAndDeleteFiles(files);
481           }
482         }, "files");
483       }
484 
485       boolean allSubdirsDeleted = true;
486       if (!subDirs.isEmpty()) {
487         final List<CleanerTask> tasks = Lists.newArrayListWithCapacity(subDirs.size());
488         for (FileStatus subdir : subDirs) {
489           CleanerTask task = new CleanerTask(subdir, false);
490           tasks.add(task);
491           task.fork();
492         }
493         allSubdirsDeleted = deleteAction(new Action<Boolean>() {
494           @Override
495           public Boolean act() throws IOException {
496             return getCleanResult(tasks);
497           }
498         }, "subdirs");
499       }
500 
501       boolean result = allFilesDeleted && allSubdirsDeleted;
502       // if and only if files and subdirs under current dir are deleted successfully, and
503       // it is not the root dir, then task will try to delete it.
504       if (result && !root) {
505         result &= deleteAction(new Action<Boolean>() {
506           @Override
507           public Boolean act() throws IOException {
508             return fs.delete(dir, false);
509           }
510         }, "dir");
511       }
512       return result;
513     }
514 
515     /**
516      * Perform a delete on a specified type.
517      * @param deletion a delete
518      * @param type possible values are 'files', 'subdirs', 'dirs'
519      * @return true if it deleted successfully, false otherwise
520      */
521     private boolean deleteAction(Action<Boolean> deletion, String type) {
522       boolean deleted;
523       try {
524         LOG.trace("Start deleting " + type + " under " + dir);
525         deleted = deletion.act();
526       } catch (PathIsNotEmptyDirectoryException exception) {
527         // N.B. HDFS throws this exception when we try to delete a non-empty directory, but
528         // LocalFileSystem throws a bare IOException. So some test code will get the verbose
529         // message below.
530         LOG.debug("Couldn't delete '" + dir + "' yet because it isn't empty. Probably transient. " +
531             "exception details at TRACE.");
532         LOG.trace("Couldn't delete '" + dir + "' yet because it isn't empty w/exception.",
533             exception);
534         deleted = false;
535       } catch (IOException ioe) {
536         LOG.info("Could not delete " + type + " under " + dir + ". might be transient; we'll " +
537             "retry. if it keeps happening, use following exception when asking on mailing list.",
538             ioe);
539         deleted = false;
540       }
541       LOG.trace("Finish deleting " + type + " under " + dir + " deleted=" + deleted);
542       return deleted;
543     }
544 
545     /**
546      * Get cleaner results of subdirs.
547      * @param tasks subdirs cleaner tasks
548      * @return true if all subdirs deleted successfully, false for patial/all failures
549      * @throws IOException something happen during computation
550      */
551     private boolean getCleanResult(List<CleanerTask> tasks) throws IOException {
552       boolean cleaned = true;
553       try {
554         for (CleanerTask task : tasks) {
555           cleaned &= task.get();
556         }
557       } catch (InterruptedException | ExecutionException e) {
558         throw new IOException(e);
559       }
560       return cleaned;
561     }
562   }
563 }