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