001/**
002 * Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2015, QOS.ch. All rights
003 * reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under either the terms of the Eclipse Public License
006 * v1.0 as published by the Eclipse Foundation
007 *
008 * or (per the licensee's choosing)
009 *
010 * under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation.
011 */
012package ch.qos.logback.core.rolling.helper;
013
014import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP;
015
016import java.io.File;
017import java.time.Instant;
018import java.util.concurrent.ExecutorService;
019import java.util.concurrent.Future;
020
021import ch.qos.logback.core.CoreConstants;
022import ch.qos.logback.core.pattern.Converter;
023import ch.qos.logback.core.pattern.LiteralConverter;
024import ch.qos.logback.core.spi.ContextAwareBase;
025import ch.qos.logback.core.util.FileSize;
026
027public class TimeBasedArchiveRemover extends ContextAwareBase implements ArchiveRemover {
028
029    static protected final long UNINITIALIZED = -1;
030    // aim for 32 days, except in case of hourly rollover, see
031    // MAX_VALUE_FOR_INACTIVITY_PERIODS
032    static protected final long INACTIVITY_TOLERANCE_IN_MILLIS = 32L * (long) CoreConstants.MILLIS_IN_ONE_DAY;
033    static final int MAX_VALUE_FOR_INACTIVITY_PERIODS = 14 * 24; // 14 days in case of hourly rollover
034
035    final FileNamePattern fileNamePattern;
036    final RollingCalendar rc;
037    private int maxHistory = CoreConstants.UNBOUNDED_HISTORY;
038    private long totalSizeCap = CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP;
039    final boolean parentClean;
040    long lastHeartBeat = UNINITIALIZED;
041
042    public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, RollingCalendar rc) {
043        this.fileNamePattern = fileNamePattern;
044        this.rc = rc;
045        this.parentClean = computeParentCleaningFlag(fileNamePattern);
046    }
047
048    int callCount = 0;
049
050    public Future<?> cleanAsynchronously(Instant now) {
051        ArchiveRemoverRunnable runnable = new ArchiveRemoverRunnable(now);
052        ExecutorService alternateExecutorService = context.getAlternateExecutorService();
053        Future<?> future = alternateExecutorService.submit(runnable);
054        return future;
055    }
056
057    /**
058     * Called from the cleaning thread.
059     *
060     * @param now
061     */
062    @Override
063    public void clean(Instant now) {
064
065        long nowInMillis = now.toEpochMilli();
066        // for a live appender periodsElapsed is expected to be 1
067        int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis);
068        lastHeartBeat = nowInMillis;
069        if (periodsElapsed > 1) {
070            addInfo("Multiple periods, i.e. " + periodsElapsed
071                    + " periods, seem to have elapsed. This is expected at application start.");
072        }
073        for (int i = 0; i < periodsElapsed; i++) {
074            int offset = getPeriodOffsetForDeletionTarget() - i;
075            Instant instantOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset);
076            cleanPeriod(instantOfPeriodToClean);
077        }
078    }
079
080    protected File[] getFilesInPeriod(Instant instantOfPeriodToClean) {
081        String filenameToDelete = fileNamePattern.convert(instantOfPeriodToClean);
082        File file2Delete = new File(filenameToDelete);
083
084        if (fileExistsAndIsFile(file2Delete)) {
085            return new File[] { file2Delete };
086        } else {
087            return new File[0];
088        }
089    }
090
091    private boolean fileExistsAndIsFile(File file2Delete) {
092        return file2Delete.exists() && file2Delete.isFile();
093    }
094
095    public void cleanPeriod(Instant instantOfPeriodToClean) {
096        File[] matchingFileArray = getFilesInPeriod(instantOfPeriodToClean);
097
098        for (File f : matchingFileArray) {
099            checkAndDeleteFile(f);
100        }
101
102        if (parentClean && matchingFileArray.length > 0) {
103            File parentDir = getParentDir(matchingFileArray[0]);
104            removeFolderIfEmpty(parentDir);
105        }
106    }
107
108    private boolean checkAndDeleteFile(File f) {
109        addInfo("deleting " + f);
110        if (f == null) {
111            addWarn("Cannot delete empty file");
112            return false;
113        } else if (!f.exists()) {
114            addWarn("Cannot delete non existent file");
115            return false;
116        }
117
118        boolean result = f.delete();
119        if (!result) {
120            addWarn("Failed to delete file " + f.toString());
121        }
122        return result;
123    }
124
125    void capTotalSize(Instant now) {
126        long totalSize = 0;
127        long totalRemoved = 0;
128        int successfulDeletions = 0;
129        int failedDeletions = 0;
130
131        for (int offset = 0; offset < maxHistory; offset++) {
132            Instant instant = rc.getEndOfNextNthPeriod(now, -offset);
133            File[] matchingFileArray = getFilesInPeriod(instant);
134            descendingSort(matchingFileArray, instant);
135            for (File f : matchingFileArray) {
136                long size = f.length();
137                totalSize += size;
138                if (totalSize > totalSizeCap) {
139                    addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size));
140
141                    boolean success = checkAndDeleteFile(f);
142                    if (success) {
143                        successfulDeletions++;
144                        totalRemoved += size;
145                    } else {
146                        failedDeletions++;
147                    }
148                }
149            }
150        }
151        if ((successfulDeletions + failedDeletions) == 0) {
152            addInfo("No removal attempts were made.");
153        } else {
154            addInfo("Removed  " + new FileSize(totalRemoved) + " of files in " + successfulDeletions + " files.");
155            if (failedDeletions != 0) {
156                addInfo("There were " + failedDeletions + " failed deletion attempts.");
157            }
158        }
159    }
160
161    protected void descendingSort(File[] matchingFileArray, Instant instant) {
162        // nothing to do in super class
163    }
164
165    File getParentDir(File file) {
166        File absolute = file.getAbsoluteFile();
167        File parentDir = absolute.getParentFile();
168        return parentDir;
169    }
170
171    int computeElapsedPeriodsSinceLastClean(long nowInMillis) {
172        long periodsElapsed = 0;
173        if (lastHeartBeat == UNINITIALIZED) {
174            addInfo("first clean up after appender initialization");
175            periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS);
176            periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS);
177        } else {
178            periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis);
179            // periodsElapsed of zero is possible for size and time based policies
180        }
181        return (int) periodsElapsed;
182    }
183
184    /**
185     * Computes whether the fileNamePattern may create sub-folders.
186     *
187     * @param fileNamePattern
188     * @return
189     */
190    boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) {
191        DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter();
192        // if the date pattern has a /, then we need parent cleaning
193        if (dtc.getDatePattern().indexOf('/') != -1) {
194            return true;
195        }
196        // if the literal string after the dtc contains a /, we also
197        // need parent cleaning
198
199        Converter<Object> p = fileNamePattern.headTokenConverter;
200
201        // find the date converter
202        while (p != null) {
203            if (p instanceof DateTokenConverter) {
204                break;
205            }
206            p = p.getNext();
207        }
208
209        while (p != null) {
210            if (p instanceof LiteralConverter) {
211                String s = p.convert(null);
212                if (s.indexOf('/') != -1) {
213                    return true;
214                }
215            }
216            p = p.getNext();
217        }
218
219        // no '/', so we don't need parent cleaning
220        return false;
221    }
222
223    void removeFolderIfEmpty(File dir) {
224        removeFolderIfEmpty(dir, 0);
225    }
226
227    /**
228     * Will remove the directory passed as parameter if empty. After that, if the parent is also becomes empty, remove
229     * the parent dir as well but at most 3 times.
230     *
231     * @param dir
232     * @param depth
233     */
234    private void removeFolderIfEmpty(File dir, int depth) {
235        // we should never go more than 3 levels higher
236        if (depth >= 3) {
237            return;
238        }
239        if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) {
240            addInfo("deleting folder [" + dir + "]");
241            checkAndDeleteFile(dir);
242            removeFolderIfEmpty(dir.getParentFile(), depth + 1);
243        }
244    }
245
246    public void setMaxHistory(int maxHistory) {
247        this.maxHistory = maxHistory;
248    }
249
250    protected int getPeriodOffsetForDeletionTarget() {
251        return -maxHistory - 1;
252    }
253
254    public void setTotalSizeCap(long totalSizeCap) {
255        this.totalSizeCap = totalSizeCap;
256    }
257
258    public String toString() {
259        return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover";
260    }
261
262    public class ArchiveRemoverRunnable implements Runnable {
263        Instant now;
264
265        ArchiveRemoverRunnable(Instant now) {
266            this.now = now;
267        }
268
269        @Override
270        public void run() {
271            clean(now);
272            if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) {
273                capTotalSize(now);
274            }
275        }
276    }
277
278}