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.annotations.VisibleForTesting;
21  import com.google.common.collect.ImmutableSet;
22  import com.google.common.collect.Iterables;
23  import com.google.common.collect.Lists;
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.hadoop.conf.Configuration;
27  import org.apache.hadoop.fs.FileStatus;
28  import org.apache.hadoop.fs.FileSystem;
29  import org.apache.hadoop.fs.Path;
30  import org.apache.hadoop.hbase.ScheduledChore;
31  import org.apache.hadoop.hbase.Stoppable;
32  import org.apache.hadoop.hbase.util.FSUtils;
33  import org.apache.hadoop.ipc.RemoteException;
34  
35  import java.io.IOException;
36  import java.util.LinkedList;
37  import java.util.List;
38  import java.util.Map;
39  
40  /**
41   * Abstract Cleaner that uses a chain of delegates to clean a directory of files
42   * @param <T> Cleaner delegate class that is dynamically loaded from configuration
43   */
44  public abstract class CleanerChore<T extends FileCleanerDelegate> extends ScheduledChore {
45  
46    private static final Log LOG = LogFactory.getLog(CleanerChore.class.getName());
47  
48    private final FileSystem fs;
49    private final Path oldFileDir;
50    private final Configuration conf;
51    protected List<T> cleanersChain;
52    protected Map<String, Object> params;
53
54    public CleanerChore(String name, final int sleepPeriod, final Stoppable s, Configuration conf,
55                        FileSystem fs, Path oldFileDir, String confKey) {
56      this(name, sleepPeriod, s, conf, fs, oldFileDir, confKey, null);
57    }
58
59    /**
60     * @param name name of the chore being run
61     * @param sleepPeriod the period of time to sleep between each run
62     * @param s the stopper
63     * @param conf configuration to use
64     * @param fs handle to the FS
65     * @param oldFileDir the path to the archived files
66     * @param confKey configuration key for the classes to instantiate
67     * @param params members could be used in cleaner
68     */
69    public CleanerChore(String name, final int sleepPeriod, final Stoppable s, Configuration conf,
70        FileSystem fs, Path oldFileDir, String confKey, Map<String, Object> params) {
71      super(name, s, sleepPeriod);
72      this.fs = fs;
73      this.oldFileDir = oldFileDir;
74      this.conf = conf;
75      this.params = params;
76      initCleanerChain(confKey);
77    }
78
79  
80    /**
81     * Validate the file to see if it even belongs in the directory. If it is valid, then the file
82     * will go through the cleaner delegates, but otherwise the file is just deleted.
83     * @param file full {@link Path} of the file to be checked
84     * @return <tt>true</tt> if the file is valid, <tt>false</tt> otherwise
85     */
86    protected abstract boolean validate(Path file);
87
88    /**
89     * Instantiate and initialize all the file cleaners set in the configuration
90     * @param confKey key to get the file cleaner classes from the configuration
91     */
92    private void initCleanerChain(String confKey) {
93      this.cleanersChain = new LinkedList<T>();
94      String[] logCleaners = conf.getStrings(confKey);
95      if (logCleaners != null) {
96        for (String className : logCleaners) {
97          T logCleaner = newFileCleaner(className, conf);
98          if (logCleaner != null) {
99            LOG.debug("initialize cleaner=" + className);
100           this.cleanersChain.add(logCleaner);
101         }
102       }
103     }
104   }
105
106   /**
107    * A utility method to create new instances of LogCleanerDelegate based on the class name of the
108    * LogCleanerDelegate.
109    * @param className fully qualified class name of the LogCleanerDelegate
110    * @param conf
111    * @return the new instance
112    */
113   private T newFileCleaner(String className, Configuration conf) {
114     try {
115       Class<? extends FileCleanerDelegate> c = Class.forName(className).asSubclass(
116         FileCleanerDelegate.class);
117       @SuppressWarnings("unchecked")
118       T cleaner = (T) c.newInstance();
119       cleaner.setConf(conf);
120       cleaner.init(this.params);
121       return cleaner;
122     } catch (Exception e) {
123       LOG.warn("Can NOT create CleanerDelegate: " + className, e);
124       // skipping if can't instantiate
125       return null;
126     }
127   }
128
129   @Override
130   protected void chore() {
131     try {
132       FileStatus[] files = FSUtils.listStatus(this.fs, this.oldFileDir);
133       checkAndDeleteEntries(files);
134     } catch (IOException e) {
135       e = e instanceof RemoteException ?
136               ((RemoteException)e).unwrapRemoteException() : e;
137       LOG.warn("Error while cleaning the logs", e);
138     }
139   }
140
141   /**
142    * Loop over the given directory entries, and check whether they can be deleted.
143    * If an entry is itself a directory it will be recursively checked and deleted itself iff
144    * all subentries are deleted (and no new subentries are added in the mean time)
145    *
146    * @param entries directory entries to check
147    * @return true if all entries were successfully deleted
148    */
149   private boolean checkAndDeleteEntries(FileStatus[] entries) {
150     if (entries == null) {
151       return true;
152     }
153     boolean allEntriesDeleted = true;
154     List<FileStatus> files = Lists.newArrayListWithCapacity(entries.length);
155     for (FileStatus child : entries) {
156       Path path = child.getPath();
157       if (child.isDirectory()) {
158         // for each subdirectory delete it and all entries if possible
159         if (!checkAndDeleteDirectory(path)) {
160           allEntriesDeleted = false;
161         }
162       } else {
163         // collect all files to attempt to delete in one batch
164         files.add(child);
165       }
166     }
167     if (!checkAndDeleteFiles(files)) {
168       allEntriesDeleted = false;
169     }
170     return allEntriesDeleted;
171   }
172
173   /**
174    * Attempt to delete a directory and all files under that directory. Each child file is passed
175    * through the delegates to see if it can be deleted. If the directory has no children when the
176    * cleaners have finished it is deleted.
177    * <p>
178    * If new children files are added between checks of the directory, the directory will <b>not</b>
179    * be deleted.
180    * @param dir directory to check
181    * @return <tt>true</tt> if the directory was deleted, <tt>false</tt> otherwise.
182    */
183   @VisibleForTesting boolean checkAndDeleteDirectory(Path dir) {
184     if (LOG.isTraceEnabled()) {
185       LOG.trace("Checking directory: " + dir);
186     }
187
188     try {
189       FileStatus[] children = FSUtils.listStatus(fs, dir);
190       boolean allChildrenDeleted = checkAndDeleteEntries(children);
191
192       // if the directory still has children, we can't delete it, so we are done
193       if (!allChildrenDeleted) return false;
194     } catch (IOException e) {
195       e = e instanceof RemoteException ?
196               ((RemoteException)e).unwrapRemoteException() : e;
197       LOG.warn("Error while listing directory: " + dir, e);
198       // couldn't list directory, so don't try to delete, and don't return success
199       return false;
200     }
201
202     // otherwise, all the children (that we know about) have been deleted, so we should try to
203     // delete this directory. However, don't do so recursively so we don't delete files that have
204     // been added since we last checked.
205     try {
206       return fs.delete(dir, false);
207     } catch (IOException e) {
208       if (LOG.isTraceEnabled()) {
209         LOG.trace("Couldn't delete directory: " + dir, e);
210       }
211       // couldn't delete w/o exception, so we can't return success.
212       return false;
213     }
214   }
215
216   /**
217    * Run the given files through each of the cleaners to see if it should be deleted, deleting it if
218    * necessary.
219    * @param files List of FileStatus for the files to check (and possibly delete)
220    * @return true iff successfully deleted all files
221    */
222   private boolean checkAndDeleteFiles(List<FileStatus> files) {
223     // first check to see if the path is valid
224     List<FileStatus> validFiles = Lists.newArrayListWithCapacity(files.size());
225     List<FileStatus> invalidFiles = Lists.newArrayList();
226     for (FileStatus file : files) {
227       if (validate(file.getPath())) {
228         validFiles.add(file);
229       } else {
230         LOG.warn("Found a wrongly formatted file: " + file.getPath() + " - will delete it.");
231         invalidFiles.add(file);
232       }
233     }
234 
235     Iterable<FileStatus> deletableValidFiles = validFiles;
236     // check each of the cleaners for the valid files
237     for (T cleaner : cleanersChain) {
238       if (cleaner.isStopped() || this.getStopper().isStopped()) {
239         LOG.warn("A file cleaner" + this.getName() + " is stopped, won't delete any more files in:"
240             + this.oldFileDir);
241         return false;
242       }
243
244       Iterable<FileStatus> filteredFiles = cleaner.getDeletableFiles(deletableValidFiles);
245
246       // trace which cleaner is holding on to each file
247       if (LOG.isTraceEnabled()) {
248         ImmutableSet<FileStatus> filteredFileSet = ImmutableSet.copyOf(filteredFiles);
249         for (FileStatus file : deletableValidFiles) {
250           if (!filteredFileSet.contains(file)) {
251             LOG.trace(file.getPath() + " is not deletable according to:" + cleaner);
252           }
253         }
254       }
255
256       deletableValidFiles = filteredFiles;
257     }
258
259     Iterable<FileStatus> filesToDelete = Iterables.concat(invalidFiles, deletableValidFiles);
260     int deletedFileCount = 0;
261     for (FileStatus file : filesToDelete) {
262       Path filePath = file.getPath();
263       if (LOG.isDebugEnabled()) {
264         LOG.debug("Removing: " + filePath + " from archive");
265       }
266       try {
267         boolean success = this.fs.delete(filePath, false);
268         if (success) {
269           deletedFileCount++;
270         } else {
271           LOG.warn("Attempted to delete:" + filePath
272               + ", but couldn't. Run cleaner chain and attempt to delete on next pass.");
273         }
274       } catch (IOException e) {
275         e = e instanceof RemoteException ?
276                   ((RemoteException)e).unwrapRemoteException() : e;
277         LOG.warn("Error while deleting: " + filePath, e);
278       }
279     }
280
281     return deletedFileCount == files.size();
282   }
283
284   @Override
285   public void cleanup() {
286     for (T lc : this.cleanersChain) {
287       try {
288         lc.stop("Exiting");
289       } catch (Throwable t) {
290         LOG.warn("Stopping", t);
291       }
292     }
293   }
294 }