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.Strings; 042import org.apache.commons.lang3.tuple.Pair; 043 044import org.ametys.core.ui.Callable; 045import org.ametys.plugins.core.impl.cache.GuavaCacheStats; 046import org.ametys.runtime.i18n.I18nizableText; 047import org.ametys.runtime.plugin.component.AbstractLogEnabled; 048import org.ametys.runtime.request.RequestListener; 049import org.ametys.runtime.request.RequestListenerManager; 050 051 052/** 053 * Component that handle all the caches 054 */ 055public abstract class AbstractCacheManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable, RequestListener 056{ 057 058 /** The type of cache */ 059 public enum CacheType 060 { 061 /** A cache used for a request */ 062 REQUEST, 063 /** A cache stored in memory */ 064 MEMORY 065 } 066 067 /** Role */ 068 public static final String ROLE = AbstractCacheManager.class.getPackageName() + ".CacheManager"; 069 070 /** Map linking id with persistent caches */ 071 protected Map<String, Cache> _memoryCaches = new HashMap<>(); 072 073 /** Map linking id with CacheInfo of a request cache */ 074 protected Map<String, CacheInfo> _requestsCacheInfos = new HashMap<>(); 075 076 /** Map linking id with CacheStats of a request cache */ 077 protected Map<String, CacheStats> _requestsCacheStats = new HashMap<>(); 078 079 /** HashMap linking an id with a List of WeakReference of Caches. this mean values can be destroyed by the garbage collector */ 080 protected Map<String, List<WeakReference<Cache>>> _requestCaches = new HashMap<>(); 081 082 /** Avalon context */ 083 protected Context _context; 084 085 /** RequestListener manager */ 086 protected RequestListenerManager _requestListenerManager; 087 088 @Override 089 public void initialize() throws Exception 090 { 091 _requestListenerManager.registerListener(this); 092 } 093 094 095 @Override 096 public void service(ServiceManager serviceManager) throws ServiceException 097 { 098 _requestListenerManager = (RequestListenerManager) serviceManager.lookup(RequestListenerManager.ROLE); 099 } 100 101 102 public void requestEnded(HttpServletRequest req) 103 { 104 _requestsCacheInfos.keySet().forEach(id -> 105 { 106 Cache requestCache = req != null ? (Cache) req.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null; 107 if (requestCache != null) 108 { 109 requestCache.getId(); 110 CacheStats cacheStats = requestCache.getCacheStats(); 111 CacheStats totalCacheStats = _requestsCacheStats.get(id); 112 totalCacheStats = totalCacheStats.plus(cacheStats); 113 _requestsCacheStats.put(id, totalCacheStats); 114 } 115 }); 116 117 } 118 119 /** 120 * Called whenever a request caches' statistics need to be manually refreshed 121 * @param req the processed request 122 * @param id the if of the cache to refresh 123 */ 124 public void refreshStats(Request req, String id) 125 { 126 Cache requestCache = req != null ? (Cache) req.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null; 127 if (requestCache != null) 128 { 129 requestCache.getId(); 130 CacheStats cacheStats = requestCache.getCacheStats(); 131 CacheStats totalCacheStats = _requestsCacheStats.get(id); 132 totalCacheStats = totalCacheStats.plus(cacheStats); 133 _requestsCacheStats.put(id, totalCacheStats); 134 } 135 } 136 137 public void requestStarted(HttpServletRequest req) 138 { 139 // Nothing to do on start 140 } 141 142 public void contextualize(Context context) throws ContextException 143 { 144 _context = context; 145 } 146 147 /** 148 * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType, 149 * Create a CacheInfo to create the cache later otherwise 150 * @param id id of the cache 151 * @param name name of the cache 152 * @param description description 153 * @param computableSize true if the size of the cache can be computed 154 * @param duration the length of time after an entry is created that it should be automatically removed. Only used for MEMORY type caches 155 * @throws CacheException if a cache already exists for the id 156 */ 157 public void createMemoryCache(String id, I18nizableText name, I18nizableText description, boolean computableSize, Duration duration) throws CacheException 158 { 159 _createCache(id, name, description, CacheType.MEMORY, computableSize, duration, false); 160 } 161 162 /** 163 * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType, 164 * Create a CacheInfo to create the cache later otherwise 165 * @param id id of the cache 166 * @param name name of the cache 167 * @param description description 168 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 169 * @throws CacheException if a cache already exists for the id 170 */ 171 public void createRequestCache(String id, I18nizableText name, I18nizableText description, boolean isDispatchable) throws CacheException 172 { 173 _createCache(id, name, description, CacheType.REQUEST, false, null, isDispatchable); 174 } 175 176 /** 177 * Create a new cache and store it in memoryCache map if it's a MEMORY CacheType, 178 * Create a CacheInfo to create the cache later otherwise 179 * @param id id of the cache 180 * @param name name of the cache 181 * @param description description 182 * @param cacheType type of the cache (REQUEST or MEMORY) 183 * @param computableSize true if the size of the cache can be computed 184 * @param duration the length of time after an entry is created that it should be automatically removed. Only used for MEMORY type caches 185 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 186 * @throws CacheException if a cache already exists for the id 187 */ 188 protected void _createCache(String id, I18nizableText name, I18nizableText description, CacheType cacheType, boolean computableSize, Duration duration, boolean isDispatchable) throws CacheException 189 { 190 if (this._memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id)) 191 { 192 throw new CacheException("The cache '" + id + "' already exists"); 193 } 194 195 if (cacheType == CacheType.MEMORY) 196 { 197 Cache ametysCache = _createCache(id, name, description, computableSize, duration, false); 198 _memoryCaches.put(id, ametysCache); 199 } 200 else 201 { 202 _requestsCacheInfos.put(id, new CacheInfo(name, description, isDispatchable)); 203 _requestsCacheStats.put(id, new GuavaCacheStats()); 204 synchronized (_requestCaches) 205 { 206 _requestCaches.put(id, new ArrayList<>()); 207 } 208 } 209 } 210 211 /** 212 * Remove the cache identified by the given id. 213 * @param id id of the cache 214 * @param cacheType type of the cache 215 * @throws CacheException if the cache does not exist for the id and type 216 */ 217 public synchronized void removeCache(String id, CacheType cacheType) throws CacheException 218 { 219 switch (cacheType) 220 { 221 case MEMORY: 222 if (_memoryCaches.containsKey(id)) 223 { 224 _memoryCaches.remove(id); 225 return; 226 } 227 break; 228 case REQUEST: 229 if (_requestsCacheInfos.containsKey(id)) 230 { 231 _requestsCacheInfos.remove(id); 232 _requestCaches.remove(id); 233 _requestsCacheStats.remove(id); 234 return; 235 } 236 break; 237 default: 238 throw new IllegalStateException("Unknown CacheType " + cacheType); 239 } 240 241 throw new CacheException("The cache '" + id + "' does not exist"); 242 } 243 244 /** 245 * Get the cache by id. If it's a request cache, create it and store it in request and in _requestCaches map. 246 * @param <K> the type of the keys in cache 247 * @param <V> the type of the values in cache 248 * @param id id of the cache 249 * @return the cache related to the id 250 * @throws CacheException if no cache exist for the id 251 */ 252 @SuppressWarnings("unchecked") 253 public <K, V> Cache<K, V> get(String id) throws CacheException 254 { 255 if (!_memoryCaches.containsKey(id) && !_requestsCacheInfos.containsKey(id)) 256 { 257 throw new CacheException("Cache " + id + " does not exist "); 258 } 259 260 if (_memoryCaches.containsKey(id)) 261 { 262 return _memoryCaches.get(id); 263 } 264 else 265 { 266 Request request = null; 267 try 268 { 269 request = ContextHelper.getRequest(_context); 270 } 271 catch (CascadingRuntimeException e) 272 { 273 // Nothing... request is null 274 getLogger().debug("No request available when getting cache {}", id, e); 275 } 276 277 Cache<K, V> requestCache = request != null ? (Cache<K, V>) request.getAttribute(AbstractCacheManager.ROLE + "$" + id) : null; 278 if (requestCache == null) 279 { 280 CacheInfo cacheInfo = _requestsCacheInfos.get(id); 281 requestCache = _createCache(id, cacheInfo.getName(), cacheInfo.getDescription(), false, null, cacheInfo.isDispatchable()); 282 synchronized (_requestCaches) 283 { 284 _requestCaches.get(id).add(new WeakReference<>(requestCache)); 285 } 286 if (request != null) 287 { 288 request.setAttribute(AbstractCacheManager.ROLE + "$" + id, requestCache); 289 } 290 } 291 292 return requestCache; 293 } 294 } 295 296 /** 297 * Get all the memory caches identified by Id 298 * @return all memory caches 299 */ 300 public List<Cache> getAllMemoryCaches() 301 { 302 return new ArrayList<>(_memoryCaches.values()); 303 } 304 305 /** 306 * Get all caches classified by Identifier and CacheType. All caches includes all running request caches in any existing request. 307 * @return all cache 308 */ 309 public Map<Pair<String, CacheType>, List<Cache>> getAllCaches() 310 { 311 Map<Pair<String, CacheType>, List<Cache>> caches = new HashMap<>(); 312 313 _memoryCaches.forEach((id, cache) -> caches.put(Pair.of(id, CacheType.MEMORY), Collections.singletonList(cache))); 314 315 // clean weak references 316 synchronized (_requestCaches) 317 { 318 // Clean the list of destroyed request 319 _requestCaches.forEach((id, cacheList) -> _requestCaches.put(id, cacheList.stream().filter(wr -> wr.get() != null).collect(Collectors.toList()))); 320 321 _requestCaches.forEach((id, cacheList) -> caches.put(Pair.of(id, CacheType.REQUEST), cacheList.stream().map(wr -> wr.get()).collect(Collectors.toList()))); 322 } 323 324 return caches; 325 } 326 327 /** 328 * Get list of memory caches in JSON format 329 * @return the memory caches in JSON format 330 */ 331 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 332 public List<Map<String, Object>> getCachesAsJSONMap() 333 { 334 List<Map<String, Object>> properties = new ArrayList<>(); 335 _memoryCaches.forEach((k, v) -> 336 { 337 properties.add(v.toJSONMap(getLogger())); 338 }); 339 _requestsCacheStats.forEach((k, v) -> 340 { 341 properties.add(this.toJSONMap(k, _requestsCacheInfos.get(k), v)); 342 }); 343 return properties; 344 } 345 346 /** 347 * Returns true if the cache with given id exists 348 * @param id id of the cache 349 * @return true if the cache with given id exists 350 */ 351 public boolean hasCache(String id) 352 { 353 return _memoryCaches.containsKey(id) || _requestsCacheInfos.containsKey(id); 354 } 355 356 /** 357 * set new max size to the cache related to given id 358 * @param id the id of cache 359 * @param size the size of the cache in bytes 360 * @return true if success 361 * @throws CacheException throw CacheException if the key is null or invalid 362 * @throws UnsupportedOperationException not implemented yet 363 */ 364 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 365 public boolean setSize(String id, long size) throws CacheException, UnsupportedOperationException 366 { 367 throw new UnsupportedOperationException("NOT IMPLEMENTED YET"); 368 } 369 370 /** 371 * Create a new cache 372 * @param <K> Key type of the cache 373 * @param <V> Value type of the cache 374 * @param id the id of the cache 375 * @param name the name of the cache 376 * @param description the description of the cache 377 * @param computableSize true if the size of the cache can be computed 378 * @param duration the length of time after an entry is created that it should be automatically removed 379 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 380 * @return new cache 381 */ 382 protected abstract <K, V> Cache<K, V> _createCache(String id, I18nizableText name, I18nizableText description, boolean computableSize, Duration duration, boolean isDispatchable); 383 384 /** 385 * Encapsulation of name and description of a cache 386 */ 387 protected static final class CacheInfo 388 { 389 390 private I18nizableText _name; 391 392 private I18nizableText _description; 393 394 private boolean _isDispatchable; 395 396 /** 397 * Create new CacheInfo with name and description 398 * @param name the name of the CacheInfo 399 * @param description the description of the CacheInfo 400 * @param isDispatchable true if the cache can be transmitted in sub-requests of DispatchGenerator 401 */ 402 public CacheInfo(I18nizableText name, I18nizableText description, boolean isDispatchable) 403 { 404 _name = name; 405 _description = description; 406 _isDispatchable = isDispatchable; 407 } 408 409 /** 410 * Get the name of the CacheInfo 411 * @return the name of the CacheInfo 412 */ 413 public I18nizableText getName() 414 { 415 return _name; 416 } 417 418 /** 419 * Get the description of the CacheInfo 420 * @return the description of the CacheInfo 421 */ 422 public I18nizableText getDescription() 423 { 424 return _description; 425 } 426 427 /** 428 * Can the cache be transmitted in sub-requests of DispatchGenerator 429 * @return true if the cache can be transmitted in sub-requests of DispatchGenerator 430 */ 431 public boolean isDispatchable() 432 { 433 return _isDispatchable; 434 } 435 436 } 437 438 private Map<String, Object> toJSONMap(String id, CacheInfo cacheInfo, CacheStats cacheStats) 439 { 440 Map<String, Object> properties = new HashMap<>(); 441 properties.put("id", id); 442 properties.put("label", cacheInfo.getName()); 443 properties.put("description", cacheInfo.getDescription()); 444 properties.put("computableSize", false); 445 properties.put("access", cacheStats.requestCount()); 446 properties.put("hit", cacheStats.hitCount()); 447 properties.put("hitRate", cacheStats.hitRate()); 448 properties.put("miss", cacheStats.missCount()); 449 properties.put("missRate", cacheStats.missRate()); 450 properties.put("nbEviction", cacheStats.evictionCount()); 451 properties.put("type", CacheType.REQUEST); 452 properties.put("currentSize", 0); 453 properties.put("maxSize", 0); 454 return properties; 455 } 456 457 /** 458 * Reset all memory caches 459 * @return the list of reseted cache's id 460 */ 461 public List<String> resetAllMemoryCaches() 462 { 463 List<String> cleanedCacheIds = new ArrayList<>(); 464 for (Cache cache : getAllMemoryCaches()) 465 { 466 cache.resetCache(); 467 468 cleanedCacheIds.add(cache.getId()); 469 } 470 return cleanedCacheIds; 471 } 472 473 /** 474 * Reset the cache with provided ids 475 * @param ids the list of cache to reset 476 * @return the ids of cache that were actually reseted 477 */ 478 public List<String> resetCaches(List<String> ids) 479 { 480 List<String> cleanedCacheIds = new ArrayList<>(); 481 for (String id : ids) 482 { 483 try 484 { 485 get(id).resetCache(); 486 cleanedCacheIds.add(id); 487 } 488 catch (CacheException e) 489 { 490 getLogger().warn("Failed to clear cache with id '{}'. No cache exists with this id.", id, e); 491 } 492 } 493 return cleanedCacheIds; 494 } 495 496 /** 497 * Reset all request caches that are marked as non dispatchable 498 */ 499 public void resetAllNonDispatchableRequestCaches() 500 { 501 Request request = null; 502 try 503 { 504 request = ContextHelper.getRequest(_context); 505 } 506 catch (CascadingRuntimeException e) 507 { 508 // Nothing... request is null 509 getLogger().debug("No request available when resetting non dispatchable request caches", e); 510 return; 511 } 512 513 @SuppressWarnings("unchecked") 514 List<String> attrNames = Collections.list(request.getAttributeNames()); 515 for (String attrName : attrNames) 516 { 517 if (attrName != null && attrName.startsWith(AbstractCacheManager.ROLE)) 518 { 519 String id = Strings.CS.replace(attrName, AbstractCacheManager.ROLE + "$", StringUtils.EMPTY); 520 Cache<Object, Object> cache = get(id); 521 if (!cache.isDispatchable()) 522 { 523 refreshStats(request, id); 524 cache.resetCache(); 525 } 526 } 527 } 528 } 529}