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.Path;
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 ? Path.of(contextTempDir.getPath(), swapFileName).toFile()
224 : Path.of(storagePath, swapFileName).toFile();
225 }
226
227
228
229
230
231
232 private void shiftFiles(int index) {
233 if (index >= maxFiles - 1) {
234 try {
235 if (Files.exists(Path.of(makeFile().getAbsolutePath() + "." + index))) {
236 Files.delete(Path.of(makeFile().getAbsolutePath() + "." + index));
237 }
238 } catch (IOException e) {
239 logger.error("Could not delete file {}",
240 Path.of(makeFile().getAbsolutePath() + "." + index).toFile().getName());
241 }
242 } else {
243 shiftFiles(index + 1);
244 File srcFile =
245 index == 0 ? makeFile() : Path.of(makeFile().getAbsolutePath() + "." + index).toFile();
246 if (Files.exists(srcFile.toPath())) {
247 File destFile = Path.of(makeFile().getAbsolutePath() + "." + (index + 1)).toFile();
248 if (!srcFile.renameTo(destFile)) {
249 logger.error("Could not rename file {} to {}", srcFile.getName(), destFile.getName());
250 }
251 }
252 }
253 }
254
255
256
257
258
259
260 public synchronized void serialize() throws InterruptedException {
261 lock.lockForCommit();
262 long start = System.currentTimeMillis();
263 try {
264 shiftFiles(0);
265 try (OutputStream os = Files.newOutputStream(makeFile().toPath())) {
266 xstream.toXML(statsData, os);
267 }
268 } catch (Exception e) {
269 logger.error("Could not write stats data to '{}'", makeFile().getAbsolutePath(), e);
270 } finally {
271 lock.releaseCommitLock();
272 logger.debug("stats serialized in {}ms", System.currentTimeMillis() - start);
273 }
274 }
275
276
277
278
279
280
281
282
283 @SuppressWarnings("unchecked")
284 private Map<String, List<XYDataItem>> deserialize(File file) {
285 Map<String, List<XYDataItem>> stats = null;
286 if (file.exists() && file.canRead()) {
287 long start = System.currentTimeMillis();
288 try {
289 try (InputStream fis = Files.newInputStream(file.toPath())) {
290 stats = (Map<String, List<XYDataItem>>) xstream.fromXML(fis);
291
292 if (stats != null) {
293
294
295
296
297
298
299
300 for (Entry<String, List<XYDataItem>> set : stats.entrySet()) {
301 List<XYDataItem> list = set.getValue();
302 if (!list.isEmpty()) {
303 XYDataItem xy = list.get(list.size() - 1);
304 list.add(new XYDataItem(xy.getX().longValue() + 1, 0));
305 list.add(new XYDataItem(System.currentTimeMillis(), 0));
306 }
307 }
308 }
309 }
310 logger.debug("stats data read in {}ms", System.currentTimeMillis() - start);
311 } catch (ExceptionInInitializerError e) {
312 if (e.getCause() != null && e.getCause().getMessage() != null && e.getCause().getMessage()
313 .contains("does not \"opens java.util\" to unnamed module")) {
314 logger.error(
315 "Stats deserialization disabled, use '--add-opens java.base/java.util=ALL-UNNAMED' to start Tomcat to enable again");
316 } else {
317 logger.error("Could not read stats data from '{}' during initialization",
318 file.getAbsolutePath(), e);
319 }
320 } catch (Exception e) {
321 logger.error("Could not read stats data from '{}'", file.getAbsolutePath(), e);
322 }
323 }
324
325 return stats;
326 }
327
328
329
330
331
332
333 public void lockForUpdate() throws InterruptedException {
334 lock.lockForUpdate();
335 }
336
337
338
339
340 public void releaseLock() {
341 lock.releaseUpdateLock();
342 }
343
344
345
346
347 @Override
348 public synchronized void afterPropertiesSet() {
349 int index = 0;
350 Map<String, List<XYDataItem>> stats;
351
352 while (true) {
353 File file =
354 index == 0 ? makeFile() : Path.of(makeFile().getAbsolutePath() + "." + index).toFile();
355 stats = deserialize(file);
356 index += 1;
357 if (stats != null || index >= maxFiles - 1) {
358 break;
359 }
360 }
361
362 if (stats != null) {
363 statsData = stats;
364 } else {
365 logger.debug("Stats data file not found. Empty file assumed.");
366 }
367
368 }
369
370 @Override
371 public void destroy() throws Exception {
372 serialize();
373 }
374
375 @Override
376 public void setApplicationContext(ApplicationContext applicationContext) {
377 WebApplicationContext wac = (WebApplicationContext) applicationContext;
378 contextTempDir = (File) wac.getServletContext().getAttribute("jakarta.servlet.context.tempdir");
379 }
380
381 }