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 can happen 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              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 }