1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
46
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
56
57
58
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
78
79
80
81
82
83
84
85
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
103
104
105
106 static int calculatePoolSize(String poolSize) {
107 if (poolSize.matches("[1-9][0-9]*")) {
108
109
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
117
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
133
134
135
136
137 protected abstract boolean validate(Path file);
138
139
140
141
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
159
160
161
162
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
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
198
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
223
224
225
226
227 private boolean checkAndDeleteFiles(List<FileStatus> files) {
228 if (files == null) {
229 return true;
230 }
231
232
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
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
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
274
275
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
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
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
368
369
370
371
372
373
374
375
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
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
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
408 if (subDirs.isEmpty()) {
409
410 boolean deleted = allFilesDeleted;
411 if (allFilesDeleted && !root) {
412 deleted = deleteAction(curDirDeletion, "dir", dir);
413 }
414 result.set(deleted);
415 return;
416 }
417
418
419
420
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
439 for (FileStatus subDir : subDirs) {
440 final FileStatus finalSubDir = subDir;
441
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
460
461
462
463
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
474
475
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 }