/** * Copyright (C) 2009-2015 Dell, Inc. * See annotations for authorship information * * ==================================================================== * 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. * ==================================================================== */ package org.dasein.cloud.util; import org.dasein.cloud.CloudProvider; import org.dasein.cloud.ProviderContext; import org.dasein.util.CalendarWrapper; import org.dasein.util.uom.time.Hour; import org.dasein.util.uom.time.Millisecond; import org.dasein.util.uom.time.TimePeriod; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * Implements efficient caching of non-changing resources so that you can minimize the number of API calls being made * to a cloud provider. This is similar to the general {@link Cache} class, except it caches only singleton objects. * <p> * Example: * </p> * <pre> * private String getToken() { * SingletonCache<String> cache = SingletonCache.getInstance(provider, "token", String.class, CacheLevel.CLOUD_ACCOUNT); * String token = cache.get(provider.getContext()); * * if( token == null ) { * // make API call to authenticate * cache.put(provider.getContext(), token); * } * return token; * } * </pre> * <p>Created by George Reese: 6/27/2013 3:52 PM</p> * @author George Reese * @version 2013.07 initial version * @since 2013.07 */ public final class SingletonCache<T> { static private final HashMap<String,SingletonCache<?>> caches = new HashMap<String, SingletonCache<?>>(); static public class CacheDelegate implements CacheMBean { @Override public void clear(@Nonnull String cacheName) { SingletonCache<?> c; synchronized( caches ) { if( caches.containsKey(cacheName) ) { //noinspection unchecked c = caches.get(cacheName); } else { return; } } c.clear(); } @Override public @Nonnull String[] getCaches() { Set<String> names; synchronized( caches ) { names = caches.keySet(); } return names.toArray(new String[names.size()]); } @Override public @Nullable CacheLevel getCacheLevel(@Nonnull String cacheName) { SingletonCache<?> c; synchronized( caches ) { if( caches.containsKey(cacheName) ) { //noinspection unchecked c = caches.get(cacheName); } else { return null; } } if( c.cloudCache != null ) { return CacheLevel.CLOUD; } else if( c.cloudAccountCache != null ) { return CacheLevel.CLOUD_ACCOUNT; } else if( c.regionCache != null ) { return CacheLevel.REGION; } else if( c.regionAccountCache != null ) { return CacheLevel.REGION_ACCOUNT; } return null; } @Override public long getNextTimeout(@Nonnull String cacheName) { SingletonCache<?> c; synchronized( caches ) { if( caches.containsKey(cacheName) ) { //noinspection unchecked c = caches.get(cacheName); } else { return System.currentTimeMillis(); } } return (c.cacheStart + c.cacheTimeout.longValue()); } @Override public long getTimeoutInSeconds(@Nonnull String cacheName) { SingletonCache<?> c; synchronized( caches ) { if( caches.containsKey(cacheName) ) { //noinspection unchecked c = caches.get(cacheName); } else { return 0L; } } return (c.cacheTimeout.longValue()/1000L); } public void setTimeoutInSeconds(@Nonnull String cacheName, @Nonnegative long timeoutInSeconds) { SingletonCache<?> c; synchronized( caches ) { if( caches.containsKey(cacheName) ) { //noinspection unchecked c = caches.get(cacheName); } else { return; } } c.cacheTimeout = new TimePeriod<Millisecond>(timeoutInSeconds, TimePeriod.MILLISECOND); } } static private class CacheEntry<T> { public long lastCacheClear; public SoftReference<T> item; public @Nonnull String toString() { return ((item == null) ? "--> empty <--" : item.toString()); } } /** * Provides access to a cache for items under the specified name. * @param provider the cloud provider object governing the cache * @param name the name of the cache * @param level the level at which these objects should be cached * @param <X> the type of the object being cached * @return a cache containing the context-sensitive cached items */ static public @Nonnull <X> SingletonCache<X> getInstance(@Nonnull CloudProvider provider, @Nonnull String name, @Nonnull CacheLevel level) { return getInstance(provider, name, level, new TimePeriod<Hour>(1, TimePeriod.HOUR)); } /** * Provides access to a cache for items under the specified name. * @param provider the cloud provider object governing the cache * @param name the name of the cache * @param level the level at which these objects should be cached * @param timeout the amount of time before the cache is automatically considered stale and forces you to reload from API * @param <X> the type of the object being cached * @return a cache containing the context-sensitive cached items */ static public @Nonnull <X> SingletonCache<X> getInstance(@Nonnull CloudProvider provider, @Nonnull String name, @Nonnull CacheLevel level, @Nonnegative TimePeriod<?> timeout) { SingletonCache<X> c; name = provider.getClass().getName() + "." + name; synchronized( caches ) { if( caches.containsKey(name) ) { //noinspection unchecked c = (SingletonCache<X>)caches.get(name); } else { c = new SingletonCache<X>(level, timeout); caches.put(name, c); } } return c; } private HashMap<String,CacheEntry<T>> cloudCache; private HashMap<String,Map<String,CacheEntry<T>>> cloudAccountCache; private HashMap<String,Map<String,CacheEntry<T>>> regionCache; private HashMap<String,Map<String,Map<String,CacheEntry<T>>>> regionAccountCache; private TimePeriod<Millisecond> cacheTimeout; private long cacheStart; private SingletonCache() { } private SingletonCache(CacheLevel level, TimePeriod<?> timeout) { switch( level ) { case CLOUD: cloudCache = new HashMap<String, CacheEntry<T>>(); break; case CLOUD_ACCOUNT: cloudAccountCache = new HashMap<String, Map<String, CacheEntry<T>>>(); break; case REGION: regionCache = new HashMap<String, Map<String, CacheEntry<T>>>(); break; case REGION_ACCOUNT: regionAccountCache = new HashMap<String, Map<String, Map<String, CacheEntry<T>>>>(); break; } cacheTimeout = (TimePeriod<Millisecond>)timeout.convertTo(TimePeriod.MILLISECOND); cacheStart = System.currentTimeMillis(); } /** * Clears out the cache across the board, regardless of context. */ public void clear() { synchronized( this ) { if( cloudCache != null ) { cloudCache.clear(); } else if( cloudAccountCache != null ) { cloudAccountCache.clear(); } else if( regionCache != null ) { regionCache.clear(); } else if( regionAccountCache != null ) { regionAccountCache.clear(); } cacheStart = System.currentTimeMillis(); } } /** * Fetches the item currently cached for the context specified. Depending on the caching level, this * method may return different values for different contexts. If the returned value is null, that means * nothing is cached and you should refetch and then cache the results. * @param ctx the context for the caching * @return the item currently in the cache if one is currently cached */ public @Nullable T get(@Nonnull ProviderContext ctx) { synchronized( this ) { if( System.currentTimeMillis() > (cacheStart + CalendarWrapper.DAY) ) { clear(); return null; } } CacheEntry<T> entry = null; String endpoint = ctx.getCloud().getEndpoint(); if( cloudCache != null ) { entry = cloudCache.get(endpoint); } else if( regionCache != null ) { Map<String,CacheEntry<T>> map = regionCache.get(endpoint); if( map != null ) { entry = map.get(ctx.getRegionId()); } } else if( cloudAccountCache != null ) { Map<String,CacheEntry<T>> map = cloudAccountCache.get(endpoint); if( map != null ) { entry = map.get(ctx.getAccountNumber()); } } else if( regionAccountCache != null ) { Map<String,Map<String,CacheEntry<T>>> map = regionAccountCache.get(endpoint); if( map != null ) { Map<String,CacheEntry<T>> rmap = map.get(ctx.getRegionId()); if( rmap != null ) { entry = rmap.get(ctx.getAccountNumber()); } } } if( entry == null ) { return null; } if( entry.lastCacheClear + cacheTimeout.longValue() < System.currentTimeMillis() ) { entry.item = null; return null; } return entry.item.get(); } /** * Places a singleton item into the cache for the specified context. * @param ctx the context of the cache * @param item the item to be cached */ public void put(@Nonnull ProviderContext ctx, @Nonnull T item) { CacheEntry<T> entry = new CacheEntry<T>(); String endpoint = ctx.getCloud().getEndpoint(); entry.item = new SoftReference<T>(item); entry.lastCacheClear = System.currentTimeMillis(); if( cloudCache != null ) { cloudCache.put(endpoint, entry); } else if( regionCache != null ) { Map<String,CacheEntry<T>> map = regionCache.get(endpoint); if( map == null ) { map = new HashMap<String, CacheEntry<T>>(); regionCache.put(endpoint, map); } map.put(ctx.getRegionId(), entry); } else if( cloudAccountCache != null ) { Map<String,CacheEntry<T>> map = cloudAccountCache.get(endpoint); if( map == null ) { map = new HashMap<String, CacheEntry<T>>(); cloudAccountCache.put(endpoint, map); } map.put(ctx.getAccountNumber(), entry); } else if( regionAccountCache != null ) { Map<String,Map<String,CacheEntry<T>>> map = regionAccountCache.get(endpoint); if( map == null ) { map = new HashMap<String, Map<String, CacheEntry<T>>>(); regionAccountCache.put(endpoint, map); } Map<String,CacheEntry<T>> rmap = map.get(ctx.getRegionId()); if( rmap == null ) { rmap = new HashMap<String, CacheEntry<T>>(); map.put(ctx.getRegionId(), rmap); } rmap.put(ctx.getAccountNumber(), entry); } } }