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