package charts.builder; import java.util.Collections; import java.util.List; import java.util.Map; import play.Logger; import play.Play; import play.libs.F; import scala.concurrent.Future; import service.EventManager; import service.EventManager.EventReceiver; import service.EventManager.EventReceiverMessage; import service.OrderedEvent; import akka.actor.TypedActor; import charts.Chart; import charts.Region; 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 com.google.common.collect.Maps; public class ChartCacheImpl implements ChartCache { private final DefaultChartBuilder chartBuilder; private final Map<String, F.Promise<List<Chart>>> pmap = Maps.newHashMap(); private final Cache<String, List<Chart>> cache = CacheBuilder.newBuilder() .maximumWeight(maxSize()) .weigher(new Weigher<String, List<Chart>>() { @Override public int weigh(String k, List<Chart> charts) { return charts.size(); } }) .removalListener(new RemovalListener<String, List<Chart>>() { @Override public void onRemoval(RemovalNotification<String, List<Chart>> entry) { Logger.debug(String.format("Removing %s charts for id %s from cache", entry.getValue().size(), entry.getKey())); } }) .build(); public ChartCacheImpl(DefaultChartBuilder chartBuilder, EventManager eventManager) { this.chartBuilder = chartBuilder; final ChartCache cc = TypedActor.<ChartCache> self(); final EventReceiver er = new EventReceiver() { @Override public void end() { } @Override public void end(Throwable e) { } @Override public void push(OrderedEvent oe) { if (oe.event().type.startsWith("file:")) { // Trigger notification for event cc.cleanup(oe.event().info("id")); } } }; eventManager.tell(EventReceiverMessage.add(er, null)); } @Override public void cleanup(String fileId) { cache.invalidate(fileId); } @Override public Future<List<Chart>> getCharts(final String id) { // Check the cache final List<Chart> clist = cache.getIfPresent(id); final F.Promise<List<Chart>> promisedCharts; if (clist == null) { // Charts will need to be created, so get promise to be fulfilled promisedCharts = getPromiseOfCharts(id); } else { // Create a promise with the value already fulfilled promisedCharts = F.Promise.pure(clist); } // Return back a Scala Future (required by Akka for detecting async) // that will eventually provide the charts. return promisedCharts.wrapped(); } @Override public void update(String fileId, List<Chart> charts) { cache.put(fileId, charts); } private List<Chart> actuallyGetCharts(String id) throws Exception { return chartBuilder.getCharts(id, null, Collections.<Region>emptyList(), null); } private F.Promise<List<Chart>> getPromiseOfCharts(final String id) { // are the charts currently fetched by an already created promise? // if so reuse that promise to avoid multiple datasources to be opened. F.Promise<List<Chart>> p = pmap.get(id); if(p == null) { // Get reference to ourself, so we can update the cache asynchronously final ChartCache self = TypedActor.self(); // Create a promise based on an asynchronous operation p = F.Promise.promise(new F.Function0<List<Chart>>() { @Override public List<Chart> apply() throws Throwable { try { // Get the charts (non-modifying operation) final List<Chart> charts = actuallyGetCharts(id); // Schedule update of self with new cache value self.update(id, charts); // Fulfil promise with charts return charts; } finally { pmap.remove(id); } } }, TypedActor.dispatcher()); // Execute on our own thread-pool pmap.put(id, p); } return p; } private int maxSize() { return Play.application().configuration() .getInt("application.chartCache.size", 100); } }