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 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
139 totalSize += size;
140 if (totalSize > totalSizeCap) {
141
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
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
184 }
185 return (int) periodsElapsed;
186 }
187
188
189
190
191
192
193
194 boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) {
195 DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter();
196
197 if (dtc.getDatePattern().indexOf('/') != -1) {
198 return true;
199 }
200
201
202
203 Converter<Object> p = fileNamePattern.headTokenConverter;
204
205
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
224 return false;
225 }
226
227 void removeFolderIfEmpty(File dir) {
228 removeFolderIfEmpty(dir, 0);
229 }
230
231
232
233
234
235
236
237
238 private void removeFolderIfEmpty(File dir, int depth) {
239
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 }