/**
* Copyright 2013 Flipkart Internet, pvt ltd.
*
* 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 lego.gracekelly;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lego.gracekelly.api.CacheLoader;
import lego.gracekelly.api.CacheProvider;
import lego.gracekelly.entities.CacheEntry;
import lego.gracekelly.exceptions.CacheProviderException;
import lego.gracekelly.exceptions.KellyException;
import lego.gracekelly.helpers.Ticker;
/**
* Kelly is the primary class for Gracekelly that reloads cacheEntries when they expire
* @param <T>
*/
public class Kelly<T>{
private final CacheLoader<T> cacheLoader;
private final CacheProvider<T> cacheProvider;
private final ExecutorService executorService;
private final ConcurrentMap<String,Boolean> requestsInFlight;
/**
* The Kelly constructor takes a {@link CacheProvider}, {@link CacheLoader} and a threadpool size for cache reloading.
* @param cacheProvider
* @param cacheLoader
* @param executorPoolSize
*
* @deprecated This constructor creates unbounded queue which can make JVM go OOM.
* Instead use the constructor {@link #Kelly(CacheProvider, CacheLoader, int, int)} with defined queueSize.
*/
@Deprecated
public Kelly(CacheProvider<T> cacheProvider, CacheLoader<T> cacheLoader, int executorPoolSize){
this.cacheProvider = cacheProvider;
this.cacheLoader = cacheLoader;
executorService = Executors.newFixedThreadPool(executorPoolSize);
this.requestsInFlight = new ConcurrentHashMap<String, Boolean>();
}
/**
* The Kelly constructor takes a {@link CacheProvider}, {@link CacheLoader}, a threadPool size
* along with queueSize for cache reloading.
* @param cacheProvider
* @param cacheLoader
* @param threadPoolSize max number of threads to be used for async refresh
* @param queueSize max number of requests to be queued for async refresh
*/
public Kelly(CacheProvider<T> cacheProvider, CacheLoader<T> cacheLoader, int threadPoolSize,
int queueSize){
this.cacheProvider = cacheProvider;
this.cacheLoader = cacheLoader;
executorService = new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(queueSize),
new ThreadFactoryBuilder()
.setDaemon(false)
.setNameFormat("fk-kelly-pool-%d")
.setPriority(Thread.NORM_PRIORITY)
.build());
this.requestsInFlight = new ConcurrentHashMap<String, Boolean>();
}
/**
* Get's the {@link CacheEntry} from {@link CacheProvider}, if {@link CacheEntry} is not present then returns null.
* If the {@link CacheEntry} has expired, then a reload task is triggered through {@link CacheLoader} to reload the
* cache with the latest value for the key.
* @param key
* @return null if {@link CacheEntry} was not found by {@link CacheProvider}, else returns value.
* @throws KellyException
*/
public T get(String key) throws KellyException {
CacheEntry<T> cacheEntry = null;
try {
cacheEntry = cacheProvider.get(key);
} catch (CacheProviderException e) {
throw new KellyException(e);
}
if (cacheEntry != null){
if(cacheEntryExpired(cacheEntry)){
if(putRequestInFlight(cacheEntry))
try {
reloadCacheEntry(cacheEntry);
} catch (ExecutionException e) {
throw new KellyException(e);
} catch (InterruptedException e) {
throw new KellyException(e);
}
}
return cacheEntry.getValue();
}
else {
return null;
}
}
/**
* Put's the {@link CacheEntry} into the cache provided by {@link CacheProvider}
* @param key
* @param value
* @return true or false based on the success or failure of the operation
* @throws KellyException
*/
public boolean put(String key, CacheEntry<T> value) throws KellyException {
try {
return cacheProvider.put(key,value);
} catch (CacheProviderException e) {
throw new KellyException(e);
}
}
/**
* Expires the {@link CacheEntry} in the cache and makes a best effort to update the {@link CacheEntry} through
* {@link CacheLoader}
* @param key
* @throws KellyException
*/
public void expire(String key) throws KellyException {
T value = get(key);
/*create an expired cache entry*/
CacheEntry<T> cacheEntry = new CacheEntry<T>(key,value,-10);
/*put it into the cache, because if the refresh fails now, kelly
* will try to refresh it the next time a get is called on the entry's key*/
put(key,cacheEntry);
/*Try to refresh the entry for the given key*/
get(key);
}
/**
* Takes a {@link CacheEntry} as input and determines wether it has expired or not.
* @param cacheEntry
* @return true of the entry has expired false if it has not
*/
private boolean cacheEntryExpired(CacheEntry cacheEntry) {
if (cacheEntry.getTtl()==0)
return false;
long entryTimeStamp = cacheEntry.getEpoch_timestamp();
long currentTime = Ticker.read();
long ttl = cacheEntry.getTtl();
/* is the currentTime greater than when the CacheEntry would have expired? */
if ((entryTimeStamp+ttl) > currentTime)
return false;
else
return true;
}
/**
* Executes the LoaderCallable using the executorService.
* @param cacheEntry
* @throws ExecutionException
* @throws InterruptedException
*/
private void reloadCacheEntry(CacheEntry cacheEntry) throws ExecutionException, InterruptedException {
LoaderCallable<T> loaderCallable = new LoaderCallable<T>(this, cacheProvider, cacheLoader, cacheEntry);
executorService.submit(loaderCallable);
}
/**
* Attempts to put request in flight for a given CacheEntry to reload it
* by trying to put it in the {@link ConcurrentHashMap} using putIfAbsent
* If put is successful then return true, else return false. This ensures
* that two requests to reload the same cache entry are not in flight at
* any given time.
* @param cacheEntry
* @return true if the the request is in flight or false if the request isn't in flight.
*/
private boolean putRequestInFlight(CacheEntry<T> cacheEntry){
Object returnValue;
returnValue =requestsInFlight.putIfAbsent(cacheEntry.getKey(),true);
if (returnValue==null)
return true;
else
return false;
}
/**
* removes a request from being in flight to allow a subsequent request for
* the same key to be put in flight.
* @param cacheEntry
*/
protected void removeRequestInFlight(CacheEntry<T> cacheEntry){
requestsInFlight.remove(cacheEntry.getKey());
}
}