001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.cache; 017 018import java.lang.ref.WeakReference; 019import java.time.Duration; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.stream.Collectors; 026 027import javax.servlet.http.HttpServletRequest; 028 029import org.apache.avalon.framework.CascadingRuntimeException; 030import org.apache.avalon.framework.activity.Initializable; 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.context.Context; 033import org.apache.avalon.framework.context.ContextException; 034import org.apache.avalon.framework.context.Contextualizable; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.cocoon.components.ContextHelper; 039import org.apache.cocoon.environment.Request; 040import org.apache.commons.lang3.tuple.Pair; 041 042import org.ametys.core.ui.Callable; 043import org.ametys.plugins.core.impl.cache.GuavaCacheStats; 044import org.ametys.runtime.i18n.I18nizableText; 045import org.ametys.runtime.plugin.component.AbstractLogEnabled; 046import org.ametys.runtime.request.RequestListener; 047import org.ametys.runtime.request.RequestListenerManager; 048 049 050/** 051 * Component that handle all the caches 052 */ 053public abstract class AbstractCacheManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable, RequestListener 054{ 055 056 /** The type of cache */ 057 public enum CacheType 058 { 059 /** A cache used for a request */ 060 REQUEST, 061 /** A cache stored in memory */ 062 MEMORY 063 } 064 065 /** Role */ 066 public static final String ROLE = AbstractCacheManager.class.getPackageName() + ".CacheManager"; 067 068 /** Map linking id with persistent caches */ 069 protected Map<String, Cache> _memoryCaches = new HashMap<>(); 070 071 /** Map linking id with CacheInfo of a request cache */ 072 protected Map<String, CacheInfo> _requestsCacheInfos = new HashMap<>(); 073 074 /** Map linking id with CacheStats of a request cache */ 075 protected Map<String, CacheStats> _requestsCacheStats = new HashMap<>(); 076 077 /** HashMap linking an id with a List of WeakReference of Caches. this mean values can be destroyed by the garbage collector */ 078 protected Map<String, List<WeakReference<Cache>>> _requestCaches = new HashMap<>(); 079 080 /** Avalon context */ 081 protected Context _context; 082 083 /** RequestListener manager */ 084 protected RequestListenerManager _requestListenerManager; 085 086 087 @Override 088 public void initialize() throws Exception 089 { 090 _requestListenerManager.registerListener(this); 091 } 092 093 094 @Override 095 public void service(ServiceManager serviceManager) throws ServiceException 096 { 097 _requestListenerManager = (RequestListenerManager) serviceManager.lookup(RequestListenerManager.ROLE); 098 } 099 100 101 public void requestEnded(HttpServletRequest req) 102 { 103 _requestsCacheInfos.keySet().forEach(id -> 104 { 105 Cache requestCache = req != null ? (Cache) req.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null; 106 if (requestCache != null) 107 { 108 requestCache.getId(); 109 CacheStats cacheStats = requestCache.getCacheStats(); 110 CacheStats totalCacheStats = _requestsCacheStats.get(id); 111 totalCacheStats = totalCacheStats.plus(cacheStats); 112 _requestsCacheStats.put(id, totalCacheStats); 113 } 114 }); 115 116 } 117 118 /** 119 * Called whenever a request caches' statistics need to be manually refreshed 120 * @param req the processed request 121 * @param id the if of the cache to refresh 122 */ 123 public void refreshStats(Request req, String id) 124 { 125 Cache requestCache = req != null ? (Cache) req.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null; 126 if (requestCache != null) 127 { 128 requestCache.getId(); 129 CacheStats cacheStats = requestCache.getCacheStats(); 130 CacheStats totalCacheStats = _requestsCacheStats.get(id); 131 totalCacheStats = totalCacheStats.plus(cacheStats); 132 _requestsCacheStats.put(id, totalCacheStats); 133 } 134 } 135 136 public void requestStarted(HttpServletRequest req) 137 { 138 // Nothing to do on start 139 } 140 141 public void contextualize(Context context) throws ContextException 142 { 143 _context = context; 144 } 145 146 /** 147 * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType, 148 * Create a CacheInfo to create the cache later otherwise 149 * @param id id of the cache 150 * @param name name of the cache 151 * @param description description 152 * @param computableSize true if the size of the cache can be computed 153 * @param duration the length of time after an entry is created that it should be automatically removed. Only used for MEMORY type caches 154 * @throws CacheException if a cache already exists for the id 155 */ 156 public void createMemoryCache(String id, I18nizableText name, I18nizableText description, boolean computableSize, Duration duration) throws CacheException 157 { 158 _createCache(id, name, description, CacheType.MEMORY, computableSize, duration, false); 159 } 160 161 /** 162 * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType, 163 * Create a CacheInfo to create the cache later otherwise 164 * @param id id of the cache 165 * @param name name of the cache 166 * @param description description 167 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 168 * @throws CacheException if a cache already exists for the id 169 */ 170 public void createRequestCache(String id, I18nizableText name, I18nizableText description, boolean isDispatchable) throws CacheException 171 { 172 _createCache(id, name, description, CacheType.REQUEST, false, null, isDispatchable); 173 } 174 175 /** 176 * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType, 177 * Create a CacheInfo to create the cache later otherwise 178 * @param id id of the cache 179 * @param name name of the cache 180 * @param description description 181 * @param cacheType type of the cache (REQUEST or MEMORY) 182 * @param computableSize true if the size of the cache can be computed 183 * @param duration the length of time after an entry is created that it should be automatically removed. Only used for MEMORY type caches 184 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 185 * @throws CacheException if a cache already exists for the id 186 */ 187 protected void _createCache(String id, I18nizableText name, I18nizableText description, CacheType cacheType, boolean computableSize, Duration duration, boolean isDispatchable) throws CacheException 188 { 189 if (this._memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id)) 190 { 191 throw new CacheException("The cache '" + id + "' already exists"); 192 } 193 194 if (cacheType == CacheType.MEMORY) 195 { 196 Cache ametysCache = _createCache(id, name, description, computableSize, duration, false); 197 _memoryCaches.put(id, ametysCache); 198 } 199 else 200 { 201 _requestsCacheInfos.put(id, new CacheInfo(name, description, isDispatchable)); 202 _requestsCacheStats.put(id, new GuavaCacheStats()); 203 synchronized (_requestCaches) 204 { 205 _requestCaches.put(id, new ArrayList<>()); 206 } 207 } 208 } 209 210 /** 211 * Remove the cache identified by the given id. 212 * @param id id of the cache 213 * @param cacheType type of the cache 214 * @throws CacheException if the cache does not exist for the id and type 215 */ 216 public synchronized void removeCache(String id, CacheType cacheType) throws CacheException 217 { 218 switch (cacheType) 219 { 220 case MEMORY: 221 if (_memoryCaches.containsKey(id)) 222 { 223 _memoryCaches.remove(id); 224 return; 225 } 226 break; 227 case REQUEST: 228 if (_requestsCacheInfos.containsKey(id)) 229 { 230 _requestsCacheInfos.remove(id); 231 _requestCaches.remove(id); 232 _requestsCacheStats.remove(id); 233 return; 234 } 235 break; 236 default: 237 throw new IllegalStateException("Unknown CacheType " + cacheType); 238 } 239 240 throw new CacheException("The cache '" + id + "' does not exist"); 241 } 242 243 /** 244 * Get the cache by id. If it's a request cache, create it and store it in request and in _requestCaches map. 245 * @param <K> the type of the keys in cache 246 * @param <V> the type of the values in cache 247 * @param id id of the cache 248 * @return the cache related to the id 249 * @throws CacheException if no cache exist for the id 250 */ 251 @SuppressWarnings("unchecked") 252 public <K, V> Cache<K, V> get(String id) throws CacheException 253 { 254 if (!_memoryCaches.containsKey(id) && !_requestsCacheInfos.containsKey(id)) 255 { 256 throw new CacheException("Cache " + id + " does not exist "); 257 } 258 259 if (_memoryCaches.containsKey(id)) 260 { 261 return _memoryCaches.get(id); 262 } 263 else 264 { 265 Request request = null; 266 try 267 { 268 request = ContextHelper.getRequest(_context); 269 } 270 catch (CascadingRuntimeException e) 271 { 272 // Nothing... request is null 273 getLogger().debug("No request available when getting cache {}", id, e); 274 } 275 276 Cache<K, V> requestCache = request != null ? (Cache<K, V>) request.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null; 277 if (requestCache == null) 278 { 279 CacheInfo cacheInfo = _requestsCacheInfos.get(id); 280 requestCache = _createCache(id, cacheInfo.getName(), cacheInfo.getDescription(), false, null, cacheInfo.isDispatchable()); 281 synchronized (_requestCaches) 282 { 283 _requestCaches.get(id).add(new WeakReference<Cache>(requestCache)); 284 } 285 if (request != null) 286 { 287 request.setAttribute(AbstractCacheManager.ROLE + "$" + id, requestCache); 288 } 289 } 290 291 return requestCache; 292 } 293 } 294 295 /** 296 * Get all caches classified by Identifier and CacheType. All caches includes all running request caches in any existing request. 297 * @return all cache 298 */ 299 public Map<Pair<String, CacheType>, List<Cache>> getAllCaches() 300 { 301 Map<Pair<String, CacheType>, List<Cache>> caches = new HashMap<>(); 302 303 _memoryCaches.forEach((id, cache) -> caches.put(Pair.of(id, CacheType.MEMORY), Collections.singletonList(cache))); 304 305 // clean weak references 306 synchronized (_requestCaches) 307 { 308 // Clean the list of destroyed request 309 _requestCaches.forEach((id, cacheList) -> _requestCaches.put(id, cacheList.stream().filter(wr -> wr.get() != null).collect(Collectors.toList()))); 310 311 _requestCaches.forEach((id, cacheList) -> caches.put(Pair.of(id, CacheType.REQUEST), cacheList.stream().map(wr -> wr.get()).collect(Collectors.toList()))); 312 } 313 314 return caches; 315 } 316 317 /** 318 * Get list of memory caches in JSON format 319 * @return the memory caches in JSON format 320 */ 321 @Callable 322 public List<Map<String, Object>> getCachesAsJSONMap() 323 { 324 List<Map<String, Object>> properties = new ArrayList<>(); 325 _memoryCaches.forEach((k, v) -> 326 { 327 properties.add(v.toJSONMap(getLogger())); 328 }); 329 _requestsCacheStats.forEach((k, v) -> 330 { 331 properties.add(this.toJSONMap(k, _requestsCacheInfos.get(k), v)); 332 }); 333 return properties; 334 } 335 336 /** 337 * Returns true if the cache with given id exists 338 * @param id id of the cache 339 * @return true if the cache with given id exists 340 */ 341 public boolean hasCache(String id) 342 { 343 return _memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id); 344 } 345 346 /** 347 * set new max size to the cache related to given id 348 * @param id the id of cache 349 * @param size the size of the cache in bytes 350 * @return true if success 351 * @throws CacheException throw CacheException if the key is null or invalid 352 * @throws UnsupportedOperationException not implemented yet 353 */ 354 @Callable 355 public boolean setSize(String id, long size) throws CacheException, UnsupportedOperationException 356 { 357 throw new UnsupportedOperationException("NOT IMPLEMENTED YET"); 358 } 359 360 /** 361 * Create a new cache 362 * @param <K> Key type of the cache 363 * @param <V> Value type of the cache 364 * @param id the id of the cache 365 * @param name the name of the cache 366 * @param description the description of the cache 367 * @param computableSize true if the size of the cache can be computed 368 * @param duration the length of time after an entry is created that it should be automatically removed 369 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 370 * @return new cache 371 */ 372 protected abstract <K, V> Cache<K, V> _createCache(String id, I18nizableText name, I18nizableText description, boolean computableSize, Duration duration, boolean isDispatchable); 373 374 /** 375 * Encapsulation of name and description of a cache 376 */ 377 protected static final class CacheInfo 378 { 379 380 private I18nizableText _name; 381 382 private I18nizableText _description; 383 384 private boolean _isDispatchable; 385 386 /** 387 * Create new CacheInfo with name and description 388 * @param name the name of the CacheInfo 389 * @param description the description of the CacheInfo 390 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 391 */ 392 public CacheInfo(I18nizableText name, I18nizableText description, boolean isDispatchable) 393 { 394 _name = name; 395 _description = description; 396 _isDispatchable = isDispatchable; 397 } 398 399 /** 400 * Get the name of the CacheInfo 401 * @return the name of the CacheInfo 402 */ 403 public I18nizableText getName() 404 { 405 return _name; 406 } 407 408 /** 409 * Get the description of the CacheInfo 410 * @return the description of the CacheInfo 411 */ 412 public I18nizableText getDescription() 413 { 414 return _description; 415 } 416 417 /** 418 * Can the cache be transmitted in sub-requests of DispatchGenerator 419 * @return true if the cache can be transmitted in sub-requests of DispatchGenerator 420 */ 421 public boolean isDispatchable() 422 { 423 return _isDispatchable; 424 } 425 426 } 427 428 private Map<String, Object> toJSONMap(String id, CacheInfo cacheInfo, CacheStats cacheStats) 429 { 430 Map<String, Object> properties = new HashMap<>(); 431 properties.put("id", id); 432 properties.put("label", cacheInfo.getName()); 433 properties.put("description", cacheInfo.getDescription()); 434 properties.put("computableSize", false); 435 properties.put("access", cacheStats.requestCount()); 436 properties.put("hit", cacheStats.hitCount()); 437 properties.put("hitRate", cacheStats.hitRate()); 438 properties.put("miss", cacheStats.missCount()); 439 properties.put("missRate", cacheStats.missRate()); 440 properties.put("nbEviction", cacheStats.evictionCount()); 441 properties.put("type", CacheType.REQUEST); 442 properties.put("currentSize", 0); 443 properties.put("maxSize", 0); 444 return properties; 445 } 446}