/**
* Copyright (C) 2012-2013 Selventa, Inc.
*
* This file is part of the OpenBEL Framework.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The OpenBEL Framework is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the OpenBEL Framework. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms under LGPL v3:
*
* This license does not authorize you and you are prohibited from using the
* name, trademarks, service marks, logos or similar indicia of Selventa, Inc.,
* or, in the discretion of other licensors or authors of the program, the
* name, trademarks, service marks, logos or similar indicia of such authors or
* licensors, in any marketing or advertising materials relating to your
* distribution of the program or any covered product. This restriction does
* not waive or limit your obligation to keep intact all copyright notices set
* forth in the program as delivered to you.
*
* If you distribute the program in whole or in part, or any modified version
* of the program, and you assume contractual liability to the recipient with
* respect to the program or modified version, then you will indemnify the
* authors and licensors of the program for any liabilities that these
* contractual assumptions directly impose on those licensors and authors.
*/
package org.openbel.framework.api;
import static java.lang.String.*;
import static java.util.concurrent.Executors.*;
import static org.apache.commons.lang.RandomStringUtils.*;
import static org.openbel.framework.api.KamCacheService.LoadStatus.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.openbel.framework.api.internal.KAMCatalogDao.KamFilter;
import org.openbel.framework.api.internal.KAMCatalogDao.KamInfo;
import org.openbel.framework.common.InvalidArgument;
/**
* Default thread-safe implementation of a {@link KamCacheService}.
* <p>
* <h1>Maps</h1> This implementation maintains three separate maps internally.
* <dl>
* <dt><i>KAM map</i></dt>
* <dd>Maintains a strong reference to a {@link Kam} object by its handle (a
* string)</dd>
* <dt><i>Unfiltered map</i></dt>
* <dd>Maintains a reference to handles available by {@link KamInfo}. This
* allows calls to the interface to return a handle to the KAM held in memory
* when <i>KamFilter</i> is not provided</dd>
* <dt><i>Filtered map</i></dt>
* <dd>Maintains a reference to handles available by {@link KamInfo} and
* {@link KamFilter}. This allows calls to the interface to return a handle to
* the KAM held in memory when <i>KamFilter</i> is provided</dd>
* </dl>
* </p>
* <p>
* <h1>Thread-safety</h1> Access to any of the maps is done under a single read
* lock. Mutating any of the maps is done under a single write lock. Both locks
* are contended for in a fair fashion.
* </p>
* <p>
* <h1>Concurrent Loading</h1> The number of KAMs that can be loaded
* concurrently is controlled by the {@code CONCURRENT_LOAD} constant.
* </p>
*
* @author julianray
*/
public class DefaultKamCacheService implements KamCacheService {
private static final int CACHE_KEY_LENGTH = 24;
/*
* Effectively controls how many KAMs can be loaded concurrently.
*/
protected static final int CONCURRENT_LOAD = 4;
// Maps handles to KAMs
private final Map<String, Kam> kamMap;
// Maps unfiltered KAMs to handles
private final Map<KamInfo, String> unfltrdMap;
// Maps filtered KAMs to handles
private final Map<FilteredKAMKey, String> fltrdMap;
/** {@link KAMStore} */
protected final KAMStore kAMStore;
private final ReadLock read;
private final WriteLock write;
private final ExecutorService execSvc;
/**
* Creates a default KAM cache service with a supplied {@link KAMStore}.
*
* @param kAMStore {@link KAMStore}
* @throws InvalidArgument Thrown if {@code kamStore} is null
*/
public DefaultKamCacheService(KAMStore kAMStore) {
if (kAMStore == null) {
throw new InvalidArgument("kamStore", kAMStore);
}
this.kAMStore = kAMStore;
kamMap = new HashMap<String, Kam>();
unfltrdMap = new HashMap<KamInfo, String>();
fltrdMap = new HashMap<FilteredKAMKey, String>();
final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(true);
read = rwlock.readLock();
write = rwlock.writeLock();
execSvc = newFixedThreadPool(CONCURRENT_LOAD, new _ThreadFactory());
}
/**
* {@inheritDoc}
*/
@Override
public String cacheKam(String kamName, Kam kam) {
if (kam == null) {
throw new InvalidArgument("kam", kam);
}
if (kamName == null) {
kamName = kam.getKamInfo().getName();
if (kamName == null) {
throw new InvalidArgument("no KAM name available");
}
}
read.lock();
try {
for (Entry<String, Kam> entry : kamMap.entrySet()) {
final Kam value = entry.getValue();
if (kamName.equals(value.getKamInfo().getName())) {
return entry.getKey();
}
}
} finally {
read.unlock();
}
// Generate a key and cache it
String key = generateCacheKey();
write.lock();
try {
while (kamMap.containsKey(key)) {
key = generateCacheKey();
}
kamMap.put(key, kam);
} finally {
write.unlock();
}
return key;
}
/**
* {@inheritDoc}
*/
@Override
public Kam getKam(String handleOrName) {
Kam ret;
read.lock();
try {
ret = kamMap.get(handleOrName);
if (ret != null) {
return ret;
}
// fallback to retrieval by KAM name
for (final Kam k : kamMap.values()) {
if (k.getKamInfo().getName().equals(handleOrName)) {
return k;
}
}
return null;
} finally {
read.unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public String loadKam(KamInfo ki, KamFilter kf)
throws KamCacheServiceException {
if (ki == null) {
throw new InvalidArgument("KamInfo required");
}
String ret;
Callable<String> callable;
if (kf != null) {
// Check the fltrdMap cache first
FilteredKAMKey key = new FilteredKAMKey(ki, kf);
read.lock();
try {
ret = fltrdMap.get(key);
} finally {
read.unlock();
}
if (ret != null) {
// Cache hit
return ret;
}
// Cache miss, create a callable to defer loading
callable = new CacheCallable(ki, kf);
} else {
read.lock();
try {
ret = unfltrdMap.get(ki);
} finally {
read.unlock();
}
if (ret != null) {
// Cache hit
return ret;
}
// Cache miss, create a callable to defer loading
callable = new CacheCallable(ki, null);
}
// Block, waiting for KAM to load
try {
ret = execSvc.submit(callable).get();
} catch (ExecutionException e) {
throw new KamCacheServiceException(e);
} catch (InterruptedException e) {
throw new KamCacheServiceException(e);
}
return ret;
}
/**
* {@inheritDoc}
*/
@Override
public LoadKAMResult loadKamWithResult(KamInfo ki, KamFilter kf)
throws KamCacheServiceException {
if (ki == null) {
throw new InvalidArgument("KamInfo required");
}
String handle;
Callable<String> callable;
if (kf != null) {
// Check the fltrdMap cache first
FilteredKAMKey key = new FilteredKAMKey(ki, kf);
read.lock();
try {
handle = fltrdMap.get(key);
} finally {
read.unlock();
}
if (handle != null) {
// Cache hit
LoadKAMResult ret = new LoadKAMResult(handle, LOADED);
return ret;
}
// Cache miss, create a callable to defer loading
callable = new CacheCallable(ki, kf);
} else {
read.lock();
try {
handle = unfltrdMap.get(ki);
} finally {
read.unlock();
}
if (handle != null) {
// Cache hit
LoadKAMResult ret = new LoadKAMResult(handle, LOADED);
return ret;
}
// Cache miss, create a callable to defer loading
callable = new CacheCallable(ki, null);
}
// Submit and return
execSvc.submit(callable);
LoadKAMResult ret = new LoadKAMResult(null, LOADING);
return ret;
}
/**
* {@inheritDoc}
*/
@Override
public void releaseKam(String handle) {
if (handle == null) {
throw new InvalidArgument("handle", handle);
}
// Purge any cache entries
purgeHandle(handle);
}
/**
* Removes any cached entries for this KAM handle.
* <p>
* This method will block obtaining a write lock on the cache.
* </p>
*
* @param handle String
*/
private void purgeHandle(String handle) {
write.lock();
try {
kamMap.remove(handle);
Set<Entry<FilteredKAMKey, String>> entries = fltrdMap.entrySet();
Iterator<Entry<FilteredKAMKey, String>> iter = entries.iterator();
while (iter.hasNext()) {
Entry<FilteredKAMKey, String> next = iter.next();
if (handle.equals(next.getValue())) {
iter.remove();
}
}
Set<Entry<KamInfo, String>> entries2 = unfltrdMap.entrySet();
Iterator<Entry<KamInfo, String>> iter2 = entries2.iterator();
while (iter2.hasNext()) {
Entry<KamInfo, String> next = iter2.next();
if (handle.equals(next.getValue())) {
iter2.remove();
}
}
} finally {
write.unlock();
}
}
/**
* Generates a random string to be used as a cache key.
*
* @return {@link String}
*/
protected final static String generateCacheKey() {
return randomAlphabetic(CACHE_KEY_LENGTH);
}
private class CacheCallable implements Callable<String> {
private KamInfo ki;
private KamFilter kf;
/**
* Callable task that loads a KAM and returns its handle.
*
* @param ki {@link KamInfo}; must be non-null
* @param kf {@link KamFilter}; may be null
*/
CacheCallable(final KamInfo ki, final KamFilter kf) {
if (ki == null) {
throw new InvalidArgument("ki cannot be null");
}
this.ki = ki;
this.kf = kf;
}
/**
* {@inheritDoc}
*/
@Override
public String call() throws KAMStoreException {
String ret = null;
if (kf != null) {
// Load request is for a filtered KAM.
FilteredKAMKey key = new FilteredKAMKey(ki, kf);
read.lock();
try {
ret = fltrdMap.get(key);
} finally {
read.unlock();
}
if (ret != null) {
return ret;
}
Kam k = kAMStore.getKam(ki, kf);
write.lock();
try {
// check again - now that we're alone, just you and I
ret = fltrdMap.get(key);
if (ret != null) {
return ret;
}
// cache the filtered KAM
ret = generateCacheKey();
kamMap.put(ret, k);
fltrdMap.put(key, ret);
} finally {
write.unlock();
}
} else {
// Load request is for an unfiltered KAM.
read.lock();
try {
ret = unfltrdMap.get(ki);
} finally {
read.unlock();
}
if (ret != null) {
return ret;
}
// get an unfiltered KAM
Kam k = kAMStore.getKam(ki);
write.lock();
try {
// check again - now that we're alone, just you and I
ret = unfltrdMap.get(ki);
if (ret != null) {
return ret;
}
// cache the unfiltered KAM
ret = generateCacheKey();
kamMap.put(ret, k);
unfltrdMap.put(ki, ret);
} finally {
write.unlock();
}
}
return ret;
}
}
private static class FilteredKAMKey {
final int hash;
final KamInfo info;
final KamFilter fltr;
/**
* Filtered KAM key.
*
* @param info {@link KamInfo} assumed non-null
* @param fltr {@link KamFilter} assumed non-null
*/
FilteredKAMKey(KamInfo info, KamFilter fltr) {
this.info = info;
this.fltr = fltr;
final int prime = 31;
int hash2 = prime;
hash2 *= prime;
hash2 += info.hashCode();
hash2 *= prime;
hash2 += fltr.hashCode();
this.hash = hash2;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return hash;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof FilteredKAMKey)) return false;
FilteredKAMKey f = (FilteredKAMKey) obj;
// info is non-null by contract
if (!info.equals(f.info)) return false;
// fltr is non-null by contract
if (!fltr.equals(f.fltr)) return false;
return true;
}
}
/**
* Creates daemon threads with names {@code kam-cache-thread-%d}.
*/
private class _ThreadFactory implements ThreadFactory {
int thread_num = 0;
/**
* {@inheritDoc}
*/
@Override
public Thread newThread(Runnable r) {
final Thread t = new Thread(r);
t.setName(format("kam-cache-thread-%d", thread_num++));
t.setDaemon(true);
return t;
}
}
}