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 can happen 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 historically stale " + 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) + " on account of totalSizeCap " + totalSizeCap);
140
141                    boolean success = checkAndDeleteFile(f);
142
143                    if (success) {
144                        successfulDeletions++;
145                        totalRemoved += size;
146                    } else {
147                        failedDeletions++;
148                    }
149                }
150            }
151        }
152        if ((successfulDeletions + failedDeletions) == 0) {
153            addInfo("No removal attempts were made on account of totalSizeCap="+totalSizeCap);
154        } else {
155            addInfo("Removed  " + new FileSize(totalRemoved) + " of files in " + successfulDeletions + " files on account of totalSizeCap=" + totalSizeCap);
156            if (failedDeletions != 0) {
157                addInfo("There were " + failedDeletions + " failed deletion attempts.");
158            }
159        }
160    }
161
162    protected void descendingSort(File[] matchingFileArray, Instant instant) {
163        // nothing to do in super class
164    }
165
166    File getParentDir(File file) {
167        File absolute = file.getAbsoluteFile();
168        File parentDir = absolute.getParentFile();
169        return parentDir;
170    }
171
172    int computeElapsedPeriodsSinceLastClean(long nowInMillis) {
173        long periodsElapsed = 0;
174        if (lastHeartBeat == UNINITIALIZED) {
175            addInfo("first clean up after appender initialization");
176            periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS);
177            periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS);
178        } else {
179            periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis);
180            // periodsElapsed of zero is possible for size and time based policies
181        }
182        return (int) periodsElapsed;
183    }
184
185    /**
186     * Computes whether the fileNamePattern may create sub-folders.
187     *
188     * @param fileNamePattern
189     * @return
190     */
191    boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) {
192        DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter();
193        // if the date pattern has a /, then we need parent cleaning
194        if (dtc.getDatePattern().indexOf('/') != -1) {
195            return true;
196        }
197        // if the literal string after the dtc contains a /, we also
198        // need parent cleaning
199
200        Converter<Object> p = fileNamePattern.headTokenConverter;
201
202        // find the date converter
203        while (p != null) {
204            if (p instanceof DateTokenConverter) {
205                break;
206            }
207            p = p.getNext();
208        }
209
210        while (p != null) {
211            if (p instanceof LiteralConverter) {
212                String s = p.convert(null);
213                if (s.indexOf('/') != -1) {
214                    return true;
215                }
216            }
217            p = p.getNext();
218        }
219
220        // no '/', so we don't need parent cleaning
221        return false;
222    }
223
224    void removeFolderIfEmpty(File dir) {
225        removeFolderIfEmpty(dir, 0);
226    }
227
228    /**
229     * Will remove the directory passed as parameter if empty. After that, if the parent is also becomes empty, remove
230     * the parent dir as well but at most 3 times.
231     *
232     * @param dir
233     * @param depth
234     */
235    private void removeFolderIfEmpty(File dir, int depth) {
236        // we should never go more than 3 levels higher
237        if (depth >= 3) {
238            return;
239        }
240        if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) {
241            addInfo("deleting folder [" + dir + "]");
242            checkAndDeleteFile(dir);
243            removeFolderIfEmpty(dir.getParentFile(), depth + 1);
244        }
245    }
246
247    public void setMaxHistory(int maxHistory) {
248        this.maxHistory = maxHistory;
249    }
250
251    protected int getPeriodOffsetForDeletionTarget() {
252        return -maxHistory - 1;
253    }
254
255    public void setTotalSizeCap(long totalSizeCap) {
256        this.totalSizeCap = totalSizeCap;
257    }
258
259    public String toString() {
260        return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover";
261    }
262
263    public class ArchiveRemoverRunnable implements Runnable {
264        Instant now;
265
266        ArchiveRemoverRunnable(Instant now) {
267            this.now = now;
268        }
269
270        @Override
271        public void run() {
272            clean(now);
273            if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) {
274                capTotalSize(now);
275            }
276        }
277    }
278
279}