package com.fourspaces.featherdb.backend;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.json.JSONArray;
import com.fourspaces.featherdb.FeatherDB;
import com.fourspaces.featherdb.document.Document;
/**
* Caching backend that operates on a Least Recently Used basis. It will store X number of Documents
* by revision. Only one revision per document is stored. This class must be backed by a persistent
* Backend that this class can use to save / retrieve uncached entries. When necessary, this class will
* access the backing store using the RootCredentials, otherwise access is based upon the calling
* credentials.
* <p>
* The use of a ConcurrentLinkedQueue to store the LRU data and a ConcurrentHashMap to store the cached Documents
* makes this class thread-safe.
* <p>
* Cached items are stored as weak references, allowing them to be garbage collected if needed. This will let
* the JVM adjust the size of the cache if more memory is needed.
* <p>
* Configuration settings in coffeedb.properties:<br>
* backend.cache.class - the fully qualified class name of the backing class<br>
* backend.cache.size - the number of documents to cache (default: 5000)<br>
* sa.username - the username for ROOT access (req'd if not default)<br>
* sa.password - the password for ROOT access (req'd if not default)<br>
* @author mbreese
*
*/
public class LRUCachingBackend implements Backend {
public static final String BACKEND_CACHE_SIZE = "backend.cache.size";
public static final String BACKING_CLASS = "backend.cache.class";
protected Backend backend;
// final protected Backend cachingBackend = new InMemoryBackend();
protected Set<String> databaseNames = null;
protected int cacheMax = 5000;
protected Queue<String> lru = new ConcurrentLinkedQueue<String> ();
protected Map<String,WeakReference<Document>> cache = new ConcurrentHashMap<String,WeakReference<Document>>();
public LRUCachingBackend() {
}
public void init(FeatherDB featherDB) {
backend.init(featherDB);
String backendClassName=featherDB.getProperty(BACKING_CLASS);
if (backendClassName == null) {
throw new RuntimeException("Missing backend.cache.class value");
}
Class backendClass;
try {
backendClass = getClass().getClassLoader().loadClass(backendClassName);
this.backend = (Backend) backendClass.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
if (featherDB.getProperty(BACKEND_CACHE_SIZE)!=null) {
try {
this.cacheMax=Integer.parseInt(featherDB.getProperty(BACKEND_CACHE_SIZE));;
} catch (NumberFormatException e) {
throw new RuntimeException("Error in backend.cache.size setting",e);
}
}
this.databaseNames = backend.getDatabaseNames();
}
public void shutdown() {
backend.shutdown();
}
protected void trim() {
while (lru.size()>cacheMax) {
String key = lru.remove();
cache.remove(key);
}
}
protected void remove(String db, String id) {
lru.remove(key(db,id));
cache.remove(key(db,id));
}
protected void add(Document doc) {
lru.add(key(doc.getDatabase(),doc.getId()));
cache.put(key(doc.getDatabase(),doc.getId()),new WeakReference<Document>(doc));
trim();
}
protected Document get(String db, String id, String revision) {
if (lru.contains(key(db,id))) {
WeakReference<Document> ref=cache.get(key(db,id));
if (ref!=null && ref.get()!=null) {
Document doc = ref.get();
if (revision == null) {
lru.remove(key(db,id));
lru.add(key(db,id));
return doc;
} else if (doc.getRevision().equals(revision)) {
lru.remove(key(db,id));
lru.add(key(db,id));
return doc;
} else {
// do nothing... if we don't have the proper revision, we need to retrieve it below
// this is just a cache miss.
}
}
}
Document doc = backend.getDocument(db, id,revision);
add(doc);
return doc;
}
protected String key(String db, String id) {
return db+"/"+id;
}
public void addDatabase(String name) throws BackendException{
backend.addDatabase(name);
databaseNames = backend.getDatabaseNames();
}
public void deleteDatabase(String name) throws BackendException {
backend.deleteDatabase(name);
List<String> keysToRemove = new ArrayList<String>();
for (String key: lru)
{
if (key.startsWith(name+"/")) {
keysToRemove.add(key);
}
}
for (String key: keysToRemove) {
lru.remove(key);
cache.remove(key);
}
databaseNames = backend.getDatabaseNames();
}
public void deleteDocument(String db, String id) throws BackendException {
try {
backend.deleteDocument(db, id);
remove(db,id);
} catch (BackendException e) {
throw e;
}
}
public boolean doesDocumentExist(String db, String id) {
if (lru.contains(key(db,id))) {
return true;
}
return backend.doesDocumentExist(db, id);
}
public boolean doesDocumentRevisionExist(String db, String id, String revision) {
if (lru.contains(key(db,id))) {
WeakReference<Document> ref =cache.get(key(db,id));
if (ref!=null && ref.get()!=null) {
if (ref.get().getRevision().equals(revision)) {
return true;
}
}
}
return backend.doesDocumentRevisionExist(db, id,revision);
}
public Iterable<Document> allDocuments(final String db){
return getDocuments(db,null);
}
public Iterable<Document> getDocuments(final String db, final String[] ids) {
return backend.allDocuments(db); // no way to cache them all :)
}
public Set<String> getDatabaseNames(){
return databaseNames;
}
public Document getDocument(String db, String id){
return getDocument(db,id,null);
}
public Document getDocument(String db, String id, String rev){
return get(db,id,rev);
}
public Document saveDocument(Document doc) throws BackendException{
Document saved = backend.saveDocument(doc);
add(saved);
return saved;
}
public JSONArray getDocumentRevisions(String db, String id){
return backend.getDocumentRevisions(db,id); // no way to cache them all :)
}
public boolean doesDatabaseExist(String db) {
return backend.doesDatabaseExist(db);
}
public Map<String, Object> getDatabaseStats(String name) {
return backend.getDatabaseStats(name);
}
public void touchRevision(String database, String id, String rev) {
backend.touchRevision(database, id, rev);
}
}