View Javadoc
1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2015, QOS.ch. All rights
3    * reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under either the terms of the Eclipse Public License
6    * v1.0 as published by the Eclipse Foundation
7    *
8    * or (per the licensee's choosing)
9    *
10   * under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation.
11   */
12  package ch.qos.logback.core.rolling.helper;
13  
14  import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP;
15  
16  import java.io.File;
17  import java.time.Instant;
18  import java.util.concurrent.ExecutorService;
19  import java.util.concurrent.Future;
20  
21  import ch.qos.logback.core.CoreConstants;
22  import ch.qos.logback.core.pattern.Converter;
23  import ch.qos.logback.core.pattern.LiteralConverter;
24  import ch.qos.logback.core.spi.ContextAwareBase;
25  import ch.qos.logback.core.util.FileSize;
26  
27  public class TimeBasedArchiveRemover extends ContextAwareBase implements ArchiveRemover {
28  
29      static protected final long UNINITIALIZED = -1;
30      // aim for 32 days, except in case of hourly rollover, see
31      // MAX_VALUE_FOR_INACTIVITY_PERIODS
32      static protected final long INACTIVITY_TOLERANCE_IN_MILLIS = 32L * (long) CoreConstants.MILLIS_IN_ONE_DAY;
33      static final int MAX_VALUE_FOR_INACTIVITY_PERIODS = 14 * 24; // 14 days in case of hourly rollover
34  
35      final FileNamePattern fileNamePattern;
36      final RollingCalendar rc;
37      private int maxHistory = CoreConstants.UNBOUNDED_HISTORY;
38      private long totalSizeCap = CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP;
39      final boolean parentClean;
40      long lastHeartBeat = UNINITIALIZED;
41  
42      public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, RollingCalendar rc) {
43          this.fileNamePattern = fileNamePattern;
44          this.rc = rc;
45          this.parentClean = computeParentCleaningFlag(fileNamePattern);
46      }
47  
48      int callCount = 0;
49  
50      public Future<?> cleanAsynchronously(Instant now) {
51          ArchiveRemoverRunnable runnable = new ArchiveRemoverRunnable(now);
52          ExecutorService alternateExecutorService = context.getAlternateExecutorService();
53          Future<?> future = alternateExecutorService.submit(runnable);
54          return future;
55      }
56  
57      /**
58       * Called from the cleaning thread.
59       *
60       * @param now
61       */
62      @Override
63      public void clean(Instant now) {
64  
65          long nowInMillis = now.toEpochMilli();
66          // for a live appender periodsElapsed is expected to be 1
67          int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis);
68          lastHeartBeat = nowInMillis;
69          if (periodsElapsed > 1) {
70              addInfo("Multiple periods, i.e. " + periodsElapsed
71                      + " periods, seem to have elapsed. This is expected at application start.");
72          }
73          for (int i = 0; i < periodsElapsed; i++) {
74              int offset = getPeriodOffsetForDeletionTarget() - i;
75              Instant instantOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset);
76              cleanPeriod(instantOfPeriodToClean);
77          }
78      }
79  
80      protected File[] getFilesInPeriod(Instant instantOfPeriodToClean) {
81          String filenameToDelete = fileNamePattern.convert(instantOfPeriodToClean);
82          File file2Delete = new File(filenameToDelete);
83  
84          if (fileExistsAndIsFile(file2Delete)) {
85              return new File[] { file2Delete };
86          } else {
87              return new File[0];
88          }
89      }
90  
91      private boolean fileExistsAndIsFile(File file2Delete) {
92          return file2Delete.exists() && file2Delete.isFile();
93      }
94  
95      public void cleanPeriod(Instant instantOfPeriodToClean) {
96          File[] matchingFileArray = getFilesInPeriod(instantOfPeriodToClean);
97  
98          for (File f : matchingFileArray) {
99              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 }