GuavaCacheAdapter.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 wcm.io
* %%
* 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 io.wcm.caravan.pipeline.cache.guava.impl;
import io.wcm.caravan.pipeline.cache.CachePersistencyOptions;
import io.wcm.caravan.pipeline.cache.spi.CacheAdapter;
import java.math.BigDecimal;
import java.util.Map;
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.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import com.codahale.metrics.Counter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.Weigher;
/**
* {@link CacheAdapter} implementation for Guava.
* Provides guava {@link Cache}, which size is specified in bytes. Default cache size is 10 MB. Provide higher property
* value {@value #MAX_CACHE_SIZE_MB_PROPERTY} to set up higher cache capacity.
* items life time depends on the amount and size of stored cache items. Items, which capacity is higher than 1/4 of the
* declared cache size will not be stored.
*/
@Component(immediate = true, metatype = true,
label = "wcm.io Caravan Pipeline Cache Adapter for Guava",
description = "Configure pipeline caching in guava.")
@Service(CacheAdapter.class)
public class GuavaCacheAdapter implements CacheAdapter {
private static final Logger log = LoggerFactory.getLogger(GuavaCacheAdapter.class);
@Property(label = "Service Ranking", intValue = GuavaCacheAdapter.DEFAULT_RANKING,
description = "Used to determine the order of caching layers if you are using multiple Cache Adapters. "
+ "Fast system-internal caches should have a lower service than slower network caches, so that they are queried first.",
propertyPrivate = false)
static final String PROPERTY_RANKING = Constants.SERVICE_RANKING;
static final int DEFAULT_RANKING = 1000;
@Property(label = "Max. size in MB",
description = "Declares the maximum total amount of VM memory in Megabyte that will be used by this cache adapter.")
static final String MAX_CACHE_SIZE_MB_PROPERTY = "maxCacheSizeMB";
private static final Integer MAX_CACHE_SIZE_MB_DEFAULT = 10;
@Property(label = "Enabled",
description = "Enables or disables the whole cache adapter and all operations.",
boolValue = GuavaCacheAdapter.CACHE_ENABLED_DEFAULT)
static final String CACHE_ENABLED_PROPERTY = "enabled";
private static final boolean CACHE_ENABLED_DEFAULT = true;
/**
* 1024*1024 multiplier used to provide bytes from megabyte values to create correct cache weight
*/
private static final BigDecimal WEIGHT_MULTIPLIER = new BigDecimal(1048576);
private Cache<String, String> guavaCache;
private long cacheWeightInBytes;
private boolean enabled;
@Reference
private MetricRegistry metricRegistry;
private Timer getLatencyTimer;
private Timer putLatencyTimer;
private Counter hitsCounter;
private Counter missesCounter;
@Activate
void activate(Map<String, Object> config) {
cacheWeightInBytes = new BigDecimal(PropertiesUtil.toDouble(config.get(MAX_CACHE_SIZE_MB_PROPERTY), MAX_CACHE_SIZE_MB_DEFAULT))
.multiply(WEIGHT_MULTIPLIER).longValue();
this.guavaCache = CacheBuilder.newBuilder().weigher(new Weigher<String, String>() {
@Override
public int weigh(String key, String value) {
return getWeight(key) + getWeight(value);
}
}).maximumWeight(cacheWeightInBytes).build();
enabled = PropertiesUtil.toBoolean(config.get(CACHE_ENABLED_PROPERTY), CACHE_ENABLED_DEFAULT);
getLatencyTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "latency", "get"));
putLatencyTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "latency", "put"));
hitsCounter = metricRegistry.counter(MetricRegistry.name(getClass(), "hits"));
missesCounter = metricRegistry.counter(MetricRegistry.name(getClass(), "misses"));
}
private int getWeight(String toMeasure) {
return 8 * ((((toMeasure.length()) * 2) + 45) / 8);
}
@Deactivate
void deactivate() {
metricRegistry.remove(MetricRegistry.name(getClass(), "latency", "get"));
metricRegistry.remove(MetricRegistry.name(getClass(), "latency", "put"));
metricRegistry.remove(MetricRegistry.name(getClass(), "hits"));
metricRegistry.remove(MetricRegistry.name(getClass(), "misses"));
}
@Override
public Observable<String> get(String cacheKey, CachePersistencyOptions options) {
if (!enabled || !options.shouldUseTransientCaches()) {
return Observable.empty();
}
return Observable.create(subscriber -> {
Timer.Context context = getLatencyTimer.time();
String cacheEntry = guavaCache.getIfPresent(cacheKey);
if (cacheEntry != null) {
hitsCounter.inc();
subscriber.onNext(cacheEntry);
}
else {
missesCounter.inc();
}
context.stop();
log.trace("Succesfully retrieved document with id {}: {}", cacheKey, cacheEntry);
subscriber.onCompleted();
});
}
@Override
public void put(String cacheKey, String jsonString, CachePersistencyOptions options) {
if (!enabled || !options.shouldUseTransientCaches()) {
return;
}
Timer.Context context = putLatencyTimer.time();
guavaCache.put(cacheKey, jsonString);
context.stop();
log.trace("Succesfully put document into Guava cache with id {}:\n{}", cacheKey, jsonString);
}
}