1
2
3
4
5
6
7
8
9
10
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
31
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;
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
59
60
61
62 @Override
63 public void clean(Instant now) {
64
65 long nowInMillis = now.toEpochMilli();
66
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
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
180 }
181 return (int) periodsElapsed;
182 }
183
184
185
186
187
188
189
190 boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) {
191 DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter();
192
193 if (dtc.getDatePattern().indexOf('/') != -1) {
194 return true;
195 }
196
197
198
199 Converter<Object> p = fileNamePattern.headTokenConverter;
200
201
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
220 return false;
221 }
222
223 void removeFolderIfEmpty(File dir) {
224 removeFolderIfEmpty(dir, 0);
225 }
226
227
228
229
230
231
232
233
234 private void removeFolderIfEmpty(File dir, int depth) {
235
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 }