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.StringUtils; 041import org.apache.commons.lang3.tuple.Pair; 042 043import org.ametys.core.ui.Callable; 044import org.ametys.plugins.core.impl.cache.GuavaCacheStats; 045import org.ametys.runtime.i18n.I18nizableText; 046import org.ametys.runtime.plugin.component.AbstractLogEnabled; 047import org.ametys.runtime.request.RequestListener; 048import org.ametys.runtime.request.RequestListenerManager; 049 050 051/** 052 * Component that handle all the caches 053 */ 054public abstract class AbstractCacheManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable, RequestListener 055{ 056 057 /** The type of cache */ 058 public enum CacheType 059 { 060 /** A cache used for a request */ 061 REQUEST, 062 /** A cache stored in memory */ 063 MEMORY 064 } 065 066 /** Role */ 067 public static final String ROLE = AbstractCacheManager.class.getPackageName() + ".CacheManager"; 068 069 /** Map linking id with persistent caches */ 070 protected Map<String, Cache> _memoryCaches = new HashMap<>(); 071 072 /** Map linking id with CacheInfo of a request cache */ 073 protected Map<String, CacheInfo> _requestsCacheInfos = new HashMap<>(); 074 075 /** Map linking id with CacheStats of a request cache */ 076 protected Map<String, CacheStats> _requestsCacheStats = new HashMap<>(); 077 078 /** HashMap linking an id with a List of WeakReference of Caches. this mean values can be destroyed by the garbage collector */ 079 protected Map<String, List<WeakReference<Cache>>> _requestCaches = new HashMap<>(); 080 081 /** Avalon context */ 082 protected Context _context; 083 084 /** RequestListener manager */ 085 protected RequestListenerManager _requestListenerManager; 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<>(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 the memory caches identified by Id 297 * @return all memory caches 298 */ 299 public List<Cache> getAllMemoryCaches() 300 { 301 return new ArrayList<>(_memoryCaches.values()); 302 } 303 304 /** 305 * Get all caches classified by Identifier and CacheType. All caches includes all running request caches in any existing request. 306 * @return all cache 307 */ 308 public Map<Pair<String, CacheType>, List<Cache>> getAllCaches() 309 { 310 Map<Pair<String, CacheType>, List<Cache>> caches = new HashMap<>(); 311 312 _memoryCaches.forEach((id, cache) -> caches.put(Pair.of(id, CacheType.MEMORY), Collections.singletonList(cache))); 313 314 // clean weak references 315 synchronized (_requestCaches) 316 { 317 // Clean the list of destroyed request 318 _requestCaches.forEach((id, cacheList) -> _requestCaches.put(id, cacheList.stream().filter(wr -> wr.get() != null).collect(Collectors.toList()))); 319 320 _requestCaches.forEach((id, cacheList) -> caches.put(Pair.of(id, CacheType.REQUEST), cacheList.stream().map(wr -> wr.get()).collect(Collectors.toList()))); 321 } 322 323 return caches; 324 } 325 326 /** 327 * Get list of memory caches in JSON format 328 * @return the memory caches in JSON format 329 */ 330 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 331 public List<Map<String, Object>> getCachesAsJSONMap() 332 { 333 List<Map<String, Object>> properties = new ArrayList<>(); 334 _memoryCaches.forEach((k, v) -> 335 { 336 properties.add(v.toJSONMap(getLogger())); 337 }); 338 _requestsCacheStats.forEach((k, v) -> 339 { 340 properties.add(this.toJSONMap(k, _requestsCacheInfos.get(k), v)); 341 }); 342 return properties; 343 } 344 345 /** 346 * Returns true if the cache with given id exists 347 * @param id id of the cache 348 * @return true if the cache with given id exists 349 */ 350 public boolean hasCache(String id) 351 { 352 return _memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id); 353 } 354 355 /** 356 * set new max size to the cache related to given id 357 * @param id the id of cache 358 * @param size the size of the cache in bytes 359 * @return true if success 360 * @throws CacheException throw CacheException if the key is null or invalid 361 * @throws UnsupportedOperationException not implemented yet 362 */ 363 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 364 public boolean setSize(String id, long size) throws CacheException, UnsupportedOperationException 365 { 366 throw new UnsupportedOperationException("NOT IMPLEMENTED YET"); 367 } 368 369 /** 370 * Create a new cache 371 * @param <K> Key type of the cache 372 * @param <V> Value type of the cache 373 * @param id the id of the cache 374 * @param name the name of the cache 375 * @param description the description of the cache 376 * @param computableSize true if the size of the cache can be computed 377 * @param duration the length of time after an entry is created that it should be automatically removed 378 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 379 * @return new cache 380 */ 381 protected abstract <K, V> Cache<K, V> _createCache(String id, I18nizableText name, I18nizableText description, boolean computableSize, Duration duration, boolean isDispatchable); 382 383 /** 384 * Encapsulation of name and description of a cache 385 */ 386 protected static final class CacheInfo 387 { 388 389 private I18nizableText _name; 390 391 private I18nizableText _description; 392 393 private boolean _isDispatchable; 394 395 /** 396 * Create new CacheInfo with name and description 397 * @param name the name of the CacheInfo 398 * @param description the description of the CacheInfo 399 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 400 */ 401 public CacheInfo(I18nizableText name, I18nizableText description, boolean isDispatchable) 402 { 403 _name = name; 404 _description = description; 405 _isDispatchable = isDispatchable; 406 } 407 408 /** 409 * Get the name of the CacheInfo 410 * @return the name of the CacheInfo 411 */ 412 public I18nizableText getName() 413 { 414 return _name; 415 } 416 417 /** 418 * Get the description of the CacheInfo 419 * @return the description of the CacheInfo 420 */ 421 public I18nizableText getDescription() 422 { 423 return _description; 424 } 425 426 /** 427 * Can the cache be transmitted in sub-requests of DispatchGenerator 428 * @return true if the cache can be transmitted in sub-requests of DispatchGenerator 429 */ 430 public boolean isDispatchable() 431 { 432 return _isDispatchable; 433 } 434 435 } 436 437 private Map<String, Object> toJSONMap(String id, CacheInfo cacheInfo, CacheStats cacheStats) 438 { 439 Map<String, Object> properties = new HashMap<>(); 440 properties.put("id", id); 441 properties.put("label", cacheInfo.getName()); 442 properties.put("description", cacheInfo.getDescription()); 443 properties.put("computableSize", false); 444 properties.put("access", cacheStats.requestCount()); 445 properties.put("hit", cacheStats.hitCount()); 446 properties.put("hitRate", cacheStats.hitRate()); 447 properties.put("miss", cacheStats.missCount()); 448 properties.put("missRate", cacheStats.missRate()); 449 properties.put("nbEviction", cacheStats.evictionCount()); 450 properties.put("type", CacheType.REQUEST); 451 properties.put("currentSize", 0); 452 properties.put("maxSize", 0); 453 return properties; 454 } 455 456 /** 457 * Reset all memory caches 458 * @return the list of reseted cache's id 459 */ 460 public List<String> resetAllMemoryCaches() 461 { 462 List<String> cleanedCacheIds = new ArrayList<>(); 463 for (Cache cache : getAllMemoryCaches()) 464 { 465 cache.resetCache(); 466 467 cleanedCacheIds.add(cache.getId()); 468 } 469 return cleanedCacheIds; 470 } 471 472 /** 473 * Reset the cache with provided ids 474 * @param ids the list of cache to reset 475 * @return the ids of cache that were actually reseted 476 */ 477 public List<String> resetCaches(List<String> ids) 478 { 479 List<String> cleanedCacheIds = new ArrayList<>(); 480 for (String id : ids) 481 { 482 try 483 { 484 get(id).resetCache(); 485 cleanedCacheIds.add(id); 486 } 487 catch (CacheException e) 488 { 489 getLogger().warn("Failed to clear cache with id '{}'. No cache exists with this id.", id, e); 490 } 491 } 492 return cleanedCacheIds; 493 } 494 495 /** 496 * Reset all request caches that are marked as non dispatchable 497 */ 498 public void resetAllNonDispatchableRequestCaches() 499 { 500 Request request = null; 501 try 502 { 503 request = ContextHelper.getRequest(_context); 504 } 505 catch (CascadingRuntimeException e) 506 { 507 // Nothing... request is null 508 getLogger().debug("No request available when resetting non dispatchable request caches", e); 509 return; 510 } 511 512 @SuppressWarnings("unchecked") 513 List<String> attrNames = Collections.list(request.getAttributeNames()); 514 for (String attrName : attrNames) 515 { 516 if (attrName != null && attrName.startsWith(AbstractCacheManager.ROLE)) 517 { 518 String id = StringUtils.replace(attrName, AbstractCacheManager.ROLE + "$", StringUtils.EMPTY); 519 Cache<Object, Object> cache = get(id); 520 if (!cache.isDispatchable()) 521 { 522 refreshStats(request, id); 523 cache.resetCache(); 524 } 525 } 526 } 527 } 528}