1
2
3
4
5
6
7
8
9
10
11 package psiprobe.model.stats;
12
13 import com.thoughtworks.xstream.XStream;
14
15 import jakarta.inject.Inject;
16
17 import java.io.File;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.OutputStream;
21 import java.nio.file.Files;
22 import java.nio.file.Paths;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.TreeMap;
30
31 import org.jfree.data.xy.XYDataItem;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34 import org.springframework.beans.factory.DisposableBean;
35 import org.springframework.beans.factory.InitializingBean;
36 import org.springframework.beans.factory.annotation.Value;
37 import org.springframework.context.ApplicationContext;
38 import org.springframework.context.ApplicationContextAware;
39 import org.springframework.web.context.WebApplicationContext;
40
41 import psiprobe.tools.UpdateCommitLock;
42
43
44
45
46 public class StatsCollection implements InitializingBean, DisposableBean, ApplicationContextAware {
47
48
49 private static final Logger logger = LoggerFactory.getLogger(StatsCollection.class);
50
51
52 private Map<String, List<XYDataItem>> statsData = new TreeMap<>();
53
54
55 @Inject
56 private XStream xstream;
57
58
59 private String swapFileName;
60
61
62 private String storagePath;
63
64
65 private File contextTempDir;
66
67
68 private int maxFiles = 2;
69
70
71 private final UpdateCommitLock lock = new UpdateCommitLock();
72
73
74
75
76
77
78 public String getSwapFileName() {
79 return swapFileName;
80 }
81
82
83
84
85
86
87 @Value("stats.xml")
88 public void setSwapFileName(String swapFileName) {
89 this.swapFileName = swapFileName;
90 }
91
92
93
94
95
96
97 public String getStoragePath() {
98 return storagePath;
99 }
100
101
102
103
104
105
106
107
108 public void setStoragePath(String storagePath) {
109 this.storagePath = storagePath;
110 }
111
112
113
114
115
116
117
118
119 public synchronized boolean isCollected(String statsName) {
120 return statsData.get(statsName) != null;
121 }
122
123
124
125
126
127
128 public int getMaxFiles() {
129 return maxFiles;
130 }
131
132
133
134
135
136
137 public void setMaxFiles(int maxFiles) {
138 this.maxFiles = maxFiles > 0 ? maxFiles : 2;
139 }
140
141
142
143
144
145
146
147
148
149 public synchronized List<XYDataItem> newStats(String name, int maxElements) {
150 List<XYDataItem> stats = Collections.synchronizedList(new ArrayList<>(maxElements));
151 statsData.put(name, stats);
152 return stats;
153 }
154
155
156
157
158
159
160 public synchronized void resetStats(String name) {
161 List<XYDataItem> stats = getStats(name);
162 if (stats != null) {
163 stats.clear();
164 }
165 }
166
167
168
169
170
171
172
173
174 public synchronized List<XYDataItem> getStats(String name) {
175 return statsData.get(name);
176 }
177
178
179
180
181
182
183
184
185 public long getLastValueForStat(String statName) {
186 long statValue = 0;
187
188 List<XYDataItem> stats = getStats(statName);
189 if (stats != null && !stats.isEmpty()) {
190 XYDataItem xy = stats.get(stats.size() - 1);
191 if (xy != null && xy.getY() != null) {
192 statValue = xy.getY().longValue();
193 }
194 }
195
196 return statValue;
197 }
198
199
200
201
202
203
204
205
206
207 public synchronized Map<String, List<XYDataItem>> getStatsByPrefix(String statNamePrefix) {
208 Map<String, List<XYDataItem>> map = new HashMap<>();
209 for (Map.Entry<String, List<XYDataItem>> en : statsData.entrySet()) {
210 if (en.getKey().startsWith(statNamePrefix)) {
211 map.put(en.getKey(), en.getValue());
212 }
213 }
214 return map;
215 }
216
217
218
219
220
221
222 private File makeFile() {
223 return storagePath == null ? new File(contextTempDir, swapFileName)
224 : new File(storagePath, swapFileName);
225 }
226
227
228
229
230
231
232 private void shiftFiles(int index) {
233 if (index >= maxFiles - 1) {
234 try {
235 if (Files.exists(Paths.get(makeFile().getAbsolutePath() + "." + index))) {
236 Files.delete(Paths.get(makeFile().getAbsolutePath() + "." + index));
237 }
238 } catch (IOException e) {
239 logger.error("Could not delete file {}",
240 new File(makeFile().getAbsolutePath() + "." + index).getName());
241 }
242 } else {
243 shiftFiles(index + 1);
244 File srcFile = index == 0 ? makeFile() : new File(makeFile().getAbsolutePath() + "." + index);
245 if (Files.exists(srcFile.toPath())) {
246 File destFile = new File(makeFile().getAbsolutePath() + "." + (index + 1));
247 if (!srcFile.renameTo(destFile)) {
248 logger.error("Could not rename file {} to {}", srcFile.getName(), destFile.getName());
249 }
250 }
251 }
252 }
253
254
255
256
257
258
259 public synchronized void serialize() throws InterruptedException {
260 lock.lockForCommit();
261 long start = System.currentTimeMillis();
262 try {
263 shiftFiles(0);
264 try (OutputStream os = Files.newOutputStream(makeFile().toPath())) {
265 xstream.toXML(statsData, os);
266 }
267 } catch (Exception e) {
268 logger.error("Could not write stats data to '{}'", makeFile().getAbsolutePath(), e);
269 } finally {
270 lock.releaseCommitLock();
271 logger.debug("stats serialized in {}ms", System.currentTimeMillis() - start);
272 }
273 }
274
275
276
277
278
279
280
281
282 @SuppressWarnings("unchecked")
283 private Map<String, List<XYDataItem>> deserialize(File file) {
284 Map<String, List<XYDataItem>> stats = null;
285 if (file.exists() && file.canRead()) {
286 long start = System.currentTimeMillis();
287 try {
288 try (InputStream fis = Files.newInputStream(file.toPath())) {
289 stats = (Map<String, List<XYDataItem>>) xstream.fromXML(fis);
290
291 if (stats != null) {
292
293
294
295
296
297
298
299 for (Entry<String, List<XYDataItem>> set : stats.entrySet()) {
300 List<XYDataItem> list = set.getValue();
301 if (!list.isEmpty()) {
302 XYDataItem xy = list.get(list.size() - 1);
303 list.add(new XYDataItem(xy.getX().longValue() + 1, 0));
304 list.add(new XYDataItem(System.currentTimeMillis(), 0));
305 }
306 }
307 }
308 }
309 logger.debug("stats data read in {}ms", System.currentTimeMillis() - start);
310 } catch (ExceptionInInitializerError e) {
311 if (e.getCause() != null && e.getCause().getMessage() != null && e.getCause().getMessage()
312 .contains("does not \"opens java.util\" to unnamed module")) {
313 logger.error(
314 "Stats deserialization disabled, use '--add-opens java.base/java.util=ALL-UNNAMED' to start Tomcat to enable again");
315 } else {
316 logger.error("Could not read stats data from '{}' during initialization",
317 file.getAbsolutePath(), e);
318 }
319 } catch (Exception e) {
320 logger.error("Could not read stats data from '{}'", file.getAbsolutePath(), e);
321 }
322 }
323
324 return stats;
325 }
326
327
328
329
330
331
332 public void lockForUpdate() throws InterruptedException {
333 lock.lockForUpdate();
334 }
335
336
337
338
339 public void releaseLock() {
340 lock.releaseUpdateLock();
341 }
342
343
344
345
346 @Override
347 public synchronized void afterPropertiesSet() {
348 int index = 0;
349 Map<String, List<XYDataItem>> stats;
350
351 while (true) {
352 File file = index == 0 ? makeFile() : new File(makeFile().getAbsolutePath() + "." + index);
353 stats = deserialize(file);
354 index += 1;
355 if (stats != null || index >= maxFiles - 1) {
356 break;
357 }
358 }
359
360 if (stats != null) {
361 statsData = stats;
362 } else {
363 logger.debug("Stats data file not found. Empty file assumed.");
364 }
365
366 }
367
368 @Override
369 public void destroy() throws Exception {
370 serialize();
371 }
372
373 @Override
374 public void setApplicationContext(ApplicationContext applicationContext) {
375 WebApplicationContext wac = (WebApplicationContext) applicationContext;
376 contextTempDir = (File) wac.getServletContext().getAttribute("jakarta.servlet.context.tempdir");
377 }
378
379 }