1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.Serializable;
23 import java.lang.reflect.InvocationTargetException;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.Properties;
28
29 import net.bull.javamelody.internal.common.LOG;
30 import net.sf.ehcache.CacheManager;
31 import net.sf.ehcache.Ehcache;
32 import net.sf.ehcache.Statistics;
33 import net.sf.ehcache.config.CacheConfiguration;
34 import net.sf.ehcache.management.CacheStatistics;
35
36 /**
37  * Informations sur un cache de données.
38  * L'état d'une instance est initialisé à son instanciation et non mutable;
39  * il est donc de fait thread-safe.
40  * Cet état est celui d'un cache à un instant t.
41  * Les instances sont sérialisables pour pouvoir être transmises au serveur de collecte.
42  * Pour l'instant seul ehcache est géré.
43  * @author Emeric Vernat
44  */

45 public class CacheInformations implements Serializable {
46     private static final long serialVersionUID = -3025833425994923286L;
47     private static final boolean EHCACHE_AVAILABLE = isEhcacheAvailable();
48     private static final boolean EHCACHE_2_7 = isEhcache27();
49     private static final boolean EHCACHE_1_6 = isEhcache16();
50     private static final boolean EHCACHE_1_2 = isEhcache12();
51     private static final boolean EHCACHE_1_2_X = isEhcache12x();
52
53     private final String name;
54     private final long inMemoryObjectCount;
55     private final int inMemoryPercentUsed;
56     private final long onDiskObjectCount;
57     private final long inMemoryHits;
58     private final long cacheHits;
59     private final long cacheMisses;
60     private final String configuration;
61     private final List<?> cacheKeys;
62
63     CacheInformations(Ehcache cache, boolean includeKeys) {
64         super();
65         assert cache != null;
66         this.name = cache.getName();
67
68         if (includeKeys) {
69             this.cacheKeys = cache.getKeys();
70         } else {
71             this.cacheKeys = null;
72         }
73
74         if (EHCACHE_2_7) {
75             // Depuis ehcache 2.7.0, cache.getStatistics() retourne "StatisticsGateway" qui est nouvelle et plus "Statistics".
76             // CacheStatistics existe depuis ehcache 1.3.
77             final CacheStatistics statistics = new CacheStatistics(cache);
78             this.inMemoryObjectCount = statistics.getMemoryStoreObjectCount(); // ou cache.getStatistics().getLocalHeapSize() en v2.7.0
79             this.onDiskObjectCount = statistics.getDiskStoreObjectCount(); // ou cache.getStatistics().getLocalDiskSize() en v2.7.0
80             this.inMemoryHits = statistics.getInMemoryHits(); // ou cache.getStatistics().localHeapHitCount() en v2.7.0
81             this.cacheHits = statistics.getCacheHits(); // ou cache.getStatistics().cacheHitCount() en v2.7.0
82             this.cacheMisses = statistics.getCacheMisses(); // ou devrait être cache.getStatistics().cacheMissCount() en v2.7.0
83             // en raison du bug https://jira.terracotta.org/jira/browse/EHC-1010
84             // la valeur de l'efficacité du cache (hits/accesses) est fausse si ehcache 2.7.0
85             this.inMemoryPercentUsed = computeMemoryPercentUsed(cache);
86             this.configuration = buildConfiguration(cache);
87             return;
88         }
89
90         final Statistics statistics = cache.getStatistics();
91         assert statistics != null;
92         if (EHCACHE_1_6) {
93             // n'existent que depuis ehcache 1.6
94             this.inMemoryObjectCount = statistics.getMemoryStoreObjectCount();
95             this.onDiskObjectCount = statistics.getDiskStoreObjectCount();
96             // NB: en ehcache 1.2, la valeur de STATISTICS_ACCURACY_BEST_EFFORT n'était pas la même
97             assert statistics.getStatisticsAccuracy() == Statistics.STATISTICS_ACCURACY_BEST_EFFORT;
98         } else {
99             this.inMemoryObjectCount = cache.getMemoryStoreSize();
100             this.onDiskObjectCount = cache.getDiskStoreSize();
101         }
102         // la taille du cache en mémoire par cache.calculateInMemorySize() est trop lente
103         // pour être déterminée à chaque fois (1s pour 1Mo selon javadoc d'ehcache)
104         if (EHCACHE_1_2_X) {
105             // getInMemoryHits, getCacheHits et getCacheMisses n'existent pas en echache v1.2
106             // mais existent en v1.2.1 et v1.2.3 (présent dans hibernate v?) mais avec int comme résultat
107             // et existent depuis v1.2.4 mais avec long comme résultat
108             this.inMemoryHits = invokeStatisticsMethod(statistics, "getInMemoryHits");
109             this.cacheHits = invokeStatisticsMethod(statistics, "getCacheHits");
110             this.cacheMisses = invokeStatisticsMethod(statistics, "getCacheMisses");
111             // getCacheConfiguration et getMaxElementsOnDisk() n'existent pas en ehcache 1.2
112             this.inMemoryPercentUsed = -1;
113             this.configuration = null;
114         } else if (EHCACHE_1_2) {
115             this.inMemoryHits = -1;
116             this.cacheHits = -1;
117             this.cacheMisses = -1;
118             this.inMemoryPercentUsed = -1;
119             this.configuration = null;
120         } else {
121             this.inMemoryHits = statistics.getInMemoryHits();
122             this.cacheHits = statistics.getCacheHits();
123             this.cacheMisses = statistics.getCacheMisses();
124             this.inMemoryPercentUsed = computeMemoryPercentUsed(cache);
125             this.configuration = buildConfiguration(cache);
126         }
127     }
128
129     // on ne doit pas référencer la classe Statistics dans les déclarations de méthodes (issue 335)
130     private static long invokeStatisticsMethod(Object statistics, String methodName) {
131         try {
132             // getInMemoryHits, getCacheHits et getCacheMisses existent en v1.2.1 et v1.2.3
133             // mais avec int comme résultat et existent depuis v1.2.4 avec long comme résultat
134             // donc on cast en Number et non en Integer ou en Long
135             final Number result = (Number) Statistics.class.getMethod(methodName, (Class<?>[]) null)
136                     .invoke(statistics, (Object[]) null);
137             return result.longValue();
138         } catch (final NoSuchMethodException e) {
139             throw new IllegalArgumentException(e);
140         } catch (final InvocationTargetException e) {
141             throw new IllegalStateException(e.getCause());
142         } catch (final IllegalAccessException e) {
143             throw new IllegalStateException(e);
144         }
145     }
146
147     private static boolean isEhcacheAvailable() {
148         try {
149             Class.forName("net.sf.ehcache.Cache");
150             return true;
151         } catch (final ClassNotFoundException | NoClassDefFoundError e) {
152             // NoClassDefFoundError for issue 67
153             return false;
154         }
155     }
156
157     static List<CacheInformations> buildCacheInformationsList() {
158         if (!EHCACHE_AVAILABLE) {
159             return Collections.emptyList();
160         }
161         final List<CacheManager> allCacheManagers;
162         try {
163             allCacheManagers = new ArrayList<>(CacheManager.ALL_CACHE_MANAGERS);
164         } catch (final NoSuchFieldError e) {
165             // nécessaire pour ehcache 1.2 ou avant
166             return Collections.emptyList();
167         }
168         final List<CacheInformations> result = new ArrayList<>();
169         for (final CacheManager cacheManager : allCacheManagers) {
170             final String[] cacheNames = cacheManager.getCacheNames();
171             try {
172                 for (final String cacheName : cacheNames) {
173                     result.add(new CacheInformations(cacheManager.getEhcache(cacheName), false));
174                 }
175             } catch (final Exception e) {
176                 // Avoid Exception throwing in cache information parsing
177                 // (for example with JGroups or TransactionException: transaction not started, issue 402)
178                 // and do not log an exception for each cache
179                 LOG.debug(e.toString(), e);
180             }
181         }
182         return result;
183     }
184
185     public static CacheInformations buildCacheInformationsWithKeys(String cacheId) {
186         assert EHCACHE_AVAILABLE;
187         assert cacheId != null;
188         final List<CacheManager> allCacheManagers = new ArrayList<>(
189                 CacheManager.ALL_CACHE_MANAGERS);
190         for (final CacheManager cacheManager : allCacheManagers) {
191             final Ehcache ehcache = cacheManager.getEhcache(cacheId);
192             if (ehcache != null) {
193                 return new CacheInformations(ehcache, true);
194             }
195         }
196         throw new IllegalArgumentException("Cache not found");
197     }
198
199     private static boolean isEhcache27() {
200         try {
201             final InputStream input = Class.forName("net.sf.ehcache.Ehcache")
202                     .getResourceAsStream("/net/sf/ehcache/version.properties");
203             if (input != null) {
204                 try {
205                     try {
206                         final Properties properties = new Properties();
207                         properties.load(input);
208                         final String version = properties.getProperty("version");
209                         return "2.7".compareTo(version) <= 0 || "2.10".compareTo(version) <= 0;
210                     } finally {
211                         input.close();
212                     }
213                 } catch (final IOException e) { // NOPMD
214                     // continue
215                 }
216             }
217             // ce Class.forName est nécessaire sur le serveur de collecte
218             Class.forName("net.sf.ehcache.statistics.StatisticsGateway");
219             return true;
220         } catch (final ClassNotFoundException e) {
221             return false;
222         }
223     }
224
225     private static boolean isEhcache16() {
226         try {
227             // ce Class.forName est nécessaire sur le serveur de collecte
228             Class.forName("net.sf.ehcache.Statistics");
229             // getMemoryStoreObjectCount n'existe que depuis ehcache 1.6
230             Statistics.class.getMethod("getMemoryStoreObjectCount");
231             return true;
232         } catch (final ClassNotFoundException | NoSuchMethodException e) {
233             return false;
234         }
235     }
236
237     private static boolean isEhcache12() {
238         try {
239             // ce Class.forName est nécessaire sur le serveur de collecte
240             Class.forName("net.sf.ehcache.Ehcache");
241             // getCacheConfiguration n'existe pas en ehcache 1.2
242             Ehcache.class.getMethod("getCacheConfiguration");
243             return false;
244         } catch (final ClassNotFoundException | NoClassDefFoundError e) {
245             // NoClassDefFoundError for issue 67
246             return false;
247         } catch (final NoSuchMethodException e) {
248             return true;
249         }
250     }
251
252     private static boolean isEhcache12x() {
253         try {
254             // Statistics existe à partir d'ehcache 1.2.1
255             Class.forName("net.sf.ehcache.Statistics");
256             return isEhcache12();
257         } catch (final ClassNotFoundException e) {
258             return false;
259         }
260     }
261
262     // cache must not be typed,
263     // otherwise serialization would not work in the collector server or in jenkins scripts
264     private int computeMemoryPercentUsed(Object cache) {
265         final int maxElementsInMemory = ((Ehcache) cache).getCacheConfiguration()
266                 .getMaxElementsInMemory();
267         if (maxElementsInMemory == 0) {
268             // maxElementsInMemory peut être 0 (sans limite), cf issue 73
269             return -1;
270         }
271         return (int) (100 * inMemoryObjectCount / maxElementsInMemory);
272     }
273
274     // cache must not be typed,
275     // otherwise serialization would not work in the collector server or in jenkins scripts
276     private String buildConfiguration(Object cache) {
277         final StringBuilder sb = new StringBuilder();
278         // getCacheConfiguration() et getMaxElementsOnDisk() n'existent pas en ehcache 1.2
279         final CacheConfiguration config = ((Ehcache) cache).getCacheConfiguration();
280         sb.append("ehcache [maxElementsInMemory = ").append(config.getMaxElementsInMemory());
281         final boolean overflowToDisk = config.isOverflowToDisk();
282         sb.append(", overflowToDisk = ").append(overflowToDisk);
283         if (overflowToDisk) {
284             sb.append(", maxElementsOnDisk = ").append(config.getMaxElementsOnDisk());
285         }
286         final boolean eternal = config.isEternal();
287         sb.append(", eternal = ").append(eternal);
288         if (!eternal) {
289             sb.append(", timeToLiveSeconds = ").append(config.getTimeToLiveSeconds());
290             sb.append(", timeToIdleSeconds = ").append(config.getTimeToIdleSeconds());
291             sb.append(", memoryStoreEvictionPolicy = ")
292                     .append(config.getMemoryStoreEvictionPolicy());
293         }
294         sb.append(", diskPersistent = ").append(config.isDiskPersistent());
295         sb.append(']');
296         return sb.toString();
297     }
298
299     public String getName() {
300         return name;
301     }
302
303     public long getInMemoryObjectCount() {
304         return inMemoryObjectCount;
305     }
306
307     public long getInMemoryPercentUsed() {
308         return inMemoryPercentUsed;
309     }
310
311     public long getOnDiskObjectCount() {
312         return onDiskObjectCount;
313     }
314
315     public long getInMemoryHits() {
316         return inMemoryHits;
317     }
318
319     public long getCacheHits() {
320         return cacheHits;
321     }
322
323     public long getCacheMisses() {
324         return cacheMisses;
325     }
326
327     // efficacité en pourcentage du cache mémoire par rapport au cache disque
328     public int getInMemoryHitsRatio() {
329         if (cacheHits == 0) {
330             return -1;
331         }
332         return (int) (100 * inMemoryHits / cacheHits);
333     }
334
335     // efficacité en pourcentage du cache (mémoire + disque) par rapport au total des accès
336     public int getHitsRatio() {
337         final long accessCount = cacheHits + cacheMisses;
338         if (accessCount == 0) {
339             return -1;
340         }
341         return (int) (100 * cacheHits / accessCount);
342     }
343
344     public String getConfiguration() {
345         return configuration;
346     }
347
348     public List<?> getCacheKeys() {
349         return cacheKeys;
350     }
351
352     /** {@inheritDoc} */
353     @Override
354     public String toString() {
355         return getClass().getSimpleName() + "[name=" + getName() + ", inMemoryObjectCount="
356                 + getInMemoryObjectCount() + ", inMemoryPercentUsed=" + getInMemoryPercentUsed()
357                 + ", onDiskObjectCount=" + getOnDiskObjectCount() + ", inMemoryHitsRatio="
358                 + getInMemoryHitsRatio() + ", hitsRatio=" + getHitsRatio() + ']';
359     }
360 }
361