/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2015 Adobe * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package com.adobe.acs.commons.httpcache.store.mem.impl; import com.adobe.acs.commons.httpcache.config.HttpCacheConfig; import com.adobe.acs.commons.httpcache.engine.CacheContent; import com.adobe.acs.commons.httpcache.exception.HttpCacheDataStreamException; import com.adobe.acs.commons.httpcache.exception.HttpCacheKeyCreationException; import com.adobe.acs.commons.httpcache.keys.CacheKey; import com.adobe.acs.commons.httpcache.store.HttpCacheStore; import com.adobe.acs.commons.httpcache.store.TempSink; import com.adobe.acs.commons.util.impl.AbstractGuavaCacheMBean; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.cache.Weigher; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Service; import org.apache.sling.commons.osgi.PropertiesUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.management.DynamicMBean; import javax.management.NotCompliantMBeanException; import javax.management.openmbean.CompositeType; import javax.management.openmbean.OpenDataException; import javax.management.openmbean.OpenType; import javax.management.openmbean.SimpleType; import java.io.ByteArrayInputStream; import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; /** * In-memory cache store implementation. Uses Google Guava Cache. */ @Component(label = "ACS AEM Commons - HTTP Cache - In-Memory cache store.", description = "Cache data store implementation for in-memory storage.", metatype = true) @Properties({ @Property(name = HttpCacheStore.KEY_CACHE_STORE_TYPE, value = HttpCacheStore.VALUE_MEM_CACHE_STORE_TYPE, propertyPrivate = true), @Property(name = "jmx.objectname", value = "com.adobe.acs.httpcache:type=In Memory HTTP Cache Store", propertyPrivate = true), @Property(name = "webconsole.configurationFactory.nameHint", value = "TTL: {httpcache.cachestore.memcache.ttl}, " + "Max size in MB: {httpcache.cachestore.memcache.maxsize}", propertyPrivate = true) }) @Service(value = {DynamicMBean.class, HttpCacheStore.class}) public class MemHttpCacheStoreImpl extends AbstractGuavaCacheMBean<CacheKey, MemCachePersistenceObject> implements HttpCacheStore, MemCacheMBean { private static final Logger log = LoggerFactory.getLogger(MemHttpCacheStoreImpl.class); /** Megabyte to byte */ private static final long MEGABYTE = 1024L * 1024L; @Property(label = "TTL", description = "TTL for all entries in this cache in seconds. Default to -1 meaning no TTL.", longValue = MemHttpCacheStoreImpl.DEFAULT_TTL) private static final String PROP_TTL = "httpcache.cachestore.memcache.ttl"; private static final long DEFAULT_TTL = -1L; // Defaults to -1 meaning no TTL. private long ttl; @Property(label = "Maximum size of this store in MB", description = "Default to 10MB. If cache size goes beyond this size, least used entry will be evicted " + "" + "from the cache", longValue = MemHttpCacheStoreImpl.DEFAULT_MAX_SIZE_IN_MB) private static final String PROP_MAX_SIZE_IN_MB = "httpcache.cachestore.memcache.maxsize"; private static final long DEFAULT_MAX_SIZE_IN_MB = 10L; // Defaults to 10MB. private long maxSizeInMb; /** Cache - Uses Google Guava's cache */ private Cache<CacheKey, MemCachePersistenceObject> cache; @Activate protected void activate(Map<String, Object> configs) { // Read config and populate values. ttl = PropertiesUtil.toLong(configs.get(PROP_TTL), DEFAULT_TTL); maxSizeInMb = PropertiesUtil.toLong(configs.get(PROP_MAX_SIZE_IN_MB), DEFAULT_MAX_SIZE_IN_MB); // Initializing the cache. // If cache is present, invalidate all and reinitailize the cache. // Recording cache usage stats enabled. if (null != cache) { cache.invalidateAll(); log.info("Mem cache already present. Invalidating the cache and re-initializing it."); } if (ttl != DEFAULT_TTL) { // If ttl is present, attach it to guava cache configuration. cache = CacheBuilder.newBuilder() .maximumWeight(maxSizeInMb * MEGABYTE) .weigher(new MemCacheEntryWeigher()) .expireAfterWrite(ttl, TimeUnit.SECONDS) .removalListener(new MemCacheEntryRemovalListener()) .recordStats() .build(); } else { // If ttl is absent, go only with the maximum weight condition. cache = CacheBuilder.newBuilder() .maximumWeight(maxSizeInMb * MEGABYTE) .weigher(new MemCacheEntryWeigher()) .removalListener(new MemCacheEntryRemovalListener()) .recordStats() .build(); } log.info("MemHttpCacheStoreImpl activated / modified."); } @Deactivate protected void deactivate(Map<String, Object> configs) { cache.invalidateAll(); log.info("MemHttpCacheStoreImpl deactivated."); } /** * Removal listener for cache entry items. */ private static class MemCacheEntryRemovalListener implements RemovalListener<CacheKey, MemCachePersistenceObject> { private static final Logger log = LoggerFactory.getLogger(MemCacheEntryRemovalListener.class); @Override public void onRemoval(RemovalNotification<CacheKey, MemCachePersistenceObject> removalNotification) { log.debug("Mem cache entry for uri {} removed due to {}", removalNotification.getKey().toString(), removalNotification.getCause().name()); } } /** * Weigher for the cache entry. */ private static class MemCacheEntryWeigher implements Weigher<CacheKey, MemCachePersistenceObject> { @Override public int weigh(CacheKey memCacheKey, MemCachePersistenceObject memCachePersistenceObject) { // Size of the byte array. return memCachePersistenceObject.getBytes().length; } } //-------------------------<CacheStore interface specific implementation> @Override public void put(CacheKey key, CacheContent content) throws HttpCacheDataStreamException { cache.put(key, new MemCachePersistenceObject().buildForCaching(content.getStatus(), content.getCharEncoding(), content.getContentType(), content.getHeaders(), content.getInputDataStream())); } @Override public boolean contains(CacheKey key) { if (null == cache.getIfPresent(key)) { return false; } return true; } @Override public CacheContent getIfPresent(CacheKey key) { MemCachePersistenceObject value = cache.getIfPresent(key); if (null == value) { return null; } // Increment hit count value.incrementHitCount(); return new CacheContent(value.getStatus(), value.getCharEncoding(), value.getContentType(), value.getHeaders(), new ByteArrayInputStream(value.getBytes())); } @Override public long size() { return cache.size(); } @Override public void invalidate(CacheKey invalidationKey) { final ConcurrentMap<CacheKey, MemCachePersistenceObject> cacheAsMap = cache.asMap(); for (CacheKey key : cacheAsMap.keySet()) { if (key.isInvalidatedBy(invalidationKey)) { cache.invalidate(key); } } } @Override public void invalidateAll() { cache.invalidateAll(); } @Override public void invalidate(HttpCacheConfig cacheConfig) { ConcurrentMap<CacheKey, MemCachePersistenceObject> cacheAsMap = cache.asMap(); for (CacheKey key : cacheAsMap.keySet()) { // Match the cache key with cache config. try { if (cacheConfig.knows(key)) { // If matches, invalidate that particular key. cache.invalidate(key); } } catch (HttpCacheKeyCreationException e) { log.error("Could not invalidate HTTP cache. Falling back to full cache invalidation.", e); this.invalidateAll(); } } } @Override public TempSink createTempSink() { return new MemTempSinkImpl(); } //-------------------------<Mbean specific implementation> public MemHttpCacheStoreImpl() throws NotCompliantMBeanException { super(MemCacheMBean.class); } @Override public long getTtl() { return this.ttl; } @Override protected Cache<CacheKey, MemCachePersistenceObject> getCache() { return cache; } @Override protected long getBytesLength(MemCachePersistenceObject cacheObj) { return cacheObj.getBytes().length; } @Override protected void addCacheData(Map<String, Object> data, MemCachePersistenceObject cacheObj) { int hitCount = cacheObj.getHitCount(); long size = cacheObj.getBytes().length; data.put("Status", cacheObj.getStatus()); data.put("Size", FileUtils.byteCountToDisplaySize(size)); data.put("Content Type", cacheObj.getContentType()); data.put("Character Encoding", cacheObj.getCharEncoding()); data.put("Hits", hitCount); data.put("Total Size Served from Cache", FileUtils.byteCountToDisplaySize(hitCount * size)); } @Override protected String toString(MemCachePersistenceObject cacheObj) throws Exception{ return IOUtils.toString( new ByteArrayInputStream(cacheObj.getBytes()), cacheObj.getCharEncoding()); } @Override protected CompositeType getCacheEntryType() throws OpenDataException { return new CompositeType("Cache Entry", "Cache Entry", new String[] { "Cache Key", "Status", "Size", "Content Type", "Character Encoding", "Hits", "Total Size Served from Cache" }, new String[] { "Cache Key", "Status", "Size", "Content Type", "Character Encoding", "Hits", "Total Size Served from Cache" }, new OpenType[] { SimpleType.STRING, SimpleType.INTEGER, SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.INTEGER, SimpleType.STRING }); } }