package com.googlecode.objectify.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.KeyRange;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.annotation.Cached;
/**
* <p>A write-through memcache for Entity objects that works for both transactional
* and nontransactional sessions. Entity cacheability and expiration are determined
* by the {@code @Cached} annotation on the POJO.</p>
*
* <ul>
* <li>Caches negative results as well as positive results.</li>
* <li>Queries do not affect the cache in any way.</li>
* <li>Transactional reads bypass the cache, but successful transaction commits will update the cache.</li>
* </ul>
*
* <p>Note: There is a horrible, obscure, and utterly bizarre bug in GAE's memcache
* relating to Key serialization. It manifests in certain circumstances when a Key
* has a parent Key that has the same String name. For this reason, we use the
* keyToString method to stringify Keys as cache keys. The actual structure
* stored in the memcache will be String -> Entity.</p>
*
* @author Jeff Schnitzer <jeff@infohazard.org>
*/
public class CachingDatastoreService implements DatastoreService
{
/** Our memcache namespace */
public static final String MEMCACHE_NAMESPACE = "Objectify Cache";
/**
* This is necessary to track writes and update the cache only on successful commit.
*/
class TransactionWrapper implements Transaction
{
/** The real implementation */
Transaction raw;
/** Lazily constructed set of keys we will delete if transaction commits */
Set<Key> deferredDeletes;
/** Lazily constructed set of values we will put in the cache if the transaction commits */
Map<Key, Entity> deferredPuts;
/** */
public TransactionWrapper(Transaction raw)
{
this.raw = raw;
}
@Override
public void commit()
{
this.raw.commit();
// Only after successful commit should we modify the cache
if (this.deferredDeletes != null)
deleteFromCache(this.deferredDeletes);
if (this.deferredPuts != null)
putInCache(this.deferredPuts);
}
@Override
public String getId()
{
return this.raw.getId();
}
@Override
public boolean isActive()
{
return this.raw.isActive();
}
@Override
public void rollback()
{
this.raw.rollback();
}
@Override
public String getApp()
{
return this.raw.getApp();
}
/**
* Adds some keys which will be deleted if the commit is successful.
*/
public void deferCacheDelete(Key key)
{
Cached cachedAnno = fact.getMetadata(key).getCached();
if (cachedAnno == null)
return;
// If there was a put, we must not put it!
if (this.deferredPuts != null)
this.deferredPuts.remove(key);
if (this.deferredDeletes == null)
this.deferredDeletes = new HashSet<Key>();
this.deferredDeletes.add(key);
}
/**
* Adds some entities that will be added to the cache if the commit is successful.
*/
public void deferCachePut(Entity entity)
{
Cached cachedAnno = fact.getMetadata(entity.getKey()).getCached();
if (cachedAnno == null)
return;
Key key = entity.getKey();
// If there was a delete, we must not delete it!
if (this.deferredDeletes != null)
this.deferredDeletes.remove(key);
if (this.deferredPuts == null)
this.deferredPuts = new HashMap<Key, Entity>();
this.deferredPuts.put(key, entity);
}
}
/** Source of metadata so we know which kinds to cache */
ObjectifyFactory fact;
/** The real datastore service */
DatastoreService raw;
/** Lazily create this */
MemcacheService memcache;
/**
*/
public CachingDatastoreService(ObjectifyFactory fact, DatastoreService raw)
{
this.fact = fact;
this.raw = raw;
}
/** Use this to lazily get the memcache service */
protected MemcacheService getMemcache()
{
if (this.memcache == null)
{
this.memcache = MemcacheServiceFactory.getMemcacheService();
this.memcache.setNamespace(MEMCACHE_NAMESPACE);
}
return this.memcache;
}
/**
* Breaks down the map into groupings based on which are cacheable and for how long.
*
* @return a map of expiration to Key/Entity map for only the entities that are cacheable
*/
private Map<Integer, Map<Key, Entity>> categorize(Map<Key, Entity> entities)
{
Map<Integer, Map<Key, Entity>> result = new HashMap<Integer, Map<Key, Entity>>();
for (Map.Entry<Key, Entity> entry: entities.entrySet())
{
Cached cachedAnno = this.fact.getMetadata(entry.getKey()).getCached();
if (cachedAnno != null)
{
Integer expiry = cachedAnno.expirationSeconds();
Map<Key, Entity> grouping = result.get(expiry);
if (grouping == null)
{
grouping = new HashMap<Key, Entity>();
result.put(expiry, grouping);
}
grouping.put(entry.getKey(), entry.getValue());
}
}
return result;
}
/**
* Get values from the datastore, inserting negative results (null values) for any keys
* that are requested but don't come back.
*/
private Map<Key, Entity> getFromDatastore(Transaction txn, Set<Key> stillNeeded)
{
Map<Key, Entity> result = this.raw.get(txn, stillNeeded);
// Add null values for any keys not in the result set
if (result.size() != stillNeeded.size())
for (Key key: stillNeeded)
if (!result.containsKey(key))
result.put(key, null);
return result;
}
/** Hides the ugly casting and deals with String/Key conversion */
@SuppressWarnings("unchecked")
private Map<Key, Entity> getFromCacheRaw(Iterable<Key> keys)
{
Collection<String> keysColl = new ArrayList<String>();
for (Key key: keys)
keysColl.add(KeyFactory.keyToString(key));
Map<String, Entity> rawResults;
try {
rawResults = (Map)this.getMemcache().getAll((Collection)keysColl);
}
catch (Exception ex) {
// This should only be an issue if Google changes the serialization
// format of an Entity. It's possible, but this is just a cache so we
// can safely ignore the error.
return new HashMap<Key, Entity>();
}
Map<Key, Entity> keyMapped = new HashMap<Key, Entity>((int)(rawResults.size() * 1.5));
for(Map.Entry<String, Entity> entry: rawResults.entrySet())
keyMapped.put(KeyFactory.stringToKey(entry.getKey()), entry.getValue());
return keyMapped;
}
/**
* Get entries from cache. Ignores uncacheable keys.
*/
private Map<Key, Entity> getFromCache(Iterable<Key> keys)
{
Collection<Key> fetch = new ArrayList<Key>();
for (Key key: keys)
if (this.fact.getMetadata(key).getCached() != null)
fetch.add(key);
return this.getFromCacheRaw(fetch);
}
/**
* Puts entries in the cache with the specified expiration.
* @param expirationSeconds can be -1 to indicate "keep as long as possible".
*/
@SuppressWarnings("unchecked")
private void putInCache(Map<Key, Entity> entities, int expirationSeconds)
{
Map<String, Entity> rawMap = new HashMap<String, Entity>((int)(entities.size() * 1.5));
for (Map.Entry<Key, Entity> entry: entities.entrySet())
rawMap.put(KeyFactory.keyToString(entry.getKey()), entry.getValue());
if (expirationSeconds < 0)
this.getMemcache().putAll((Map)rawMap);
else
this.getMemcache().putAll((Map)rawMap, Expiration.byDeltaSeconds(expirationSeconds));
}
/**
* Puts entries in the cache with the appropriate expirations.
*/
private void putInCache(Map<Key, Entity> entities)
{
Map<Integer, Map<Key, Entity>> categories = this.categorize(entities);
for (Map.Entry<Integer, Map<Key, Entity>> entry: categories.entrySet())
this.putInCache(entry.getValue(), entry.getKey());
}
/**
* Deletes from the cache, ignoring any noncacheable keys
*/
@SuppressWarnings("unchecked")
private void deleteFromCache(Iterable<Key> keys)
{
Collection<String> cacheables = new ArrayList<String>();
for (Key key: keys)
if (this.fact.getMetadata(key).getCached() != null)
cacheables.add(KeyFactory.keyToString(key));
if (!cacheables.isEmpty())
this.getMemcache().deleteAll((Collection)cacheables);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#allocateIds(java.lang.String, long)
*/
@Override
public KeyRange allocateIds(String kind, long num)
{
return this.raw.allocateIds(kind, num);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#allocateIds(com.google.appengine.api.datastore.Key, java.lang.String, long)
*/
@Override
public KeyRange allocateIds(Key parent, String kind, long num)
{
return this.raw.allocateIds(parent, kind, num);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#beginTransaction()
*/
@Override
public Transaction beginTransaction()
{
return new TransactionWrapper(this.raw.beginTransaction());
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#delete(com.google.appengine.api.datastore.Key[])
*/
@Override
public void delete(Key... keys)
{
this.delete(null, keys);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#delete(java.lang.Iterable)
*/
@Override
public void delete(Iterable<Key> keys)
{
this.delete(null, keys);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#delete(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Key[])
*/
@Override
public void delete(Transaction txn, Key... keys)
{
this.delete(txn, Arrays.asList(keys));
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#delete(com.google.appengine.api.datastore.Transaction, java.lang.Iterable)
*/
@Override
public void delete(Transaction txn, Iterable<Key> keys)
{
this.raw.delete(txn, keys);
if (txn != null)
{
for (Key key: keys)
((TransactionWrapper)txn).deferCacheDelete(key);
}
else
{
this.deleteFromCache(keys);
}
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#get(com.google.appengine.api.datastore.Key)
*/
@Override
public Entity get(Key key) throws EntityNotFoundException
{
return this.get(null, key);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#get(java.lang.Iterable)
*/
@Override
public Map<Key, Entity> get(Iterable<Key> keys)
{
return this.get(null, keys);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#get(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Key)
*/
@Override
public Entity get(Transaction txn, Key key) throws EntityNotFoundException
{
Cached cachedAnnotation = this.fact.getMetadata(key).getCached();
if (txn != null || cachedAnnotation == null)
{
// Must ignore the cache when reading in a transaction since the
// transaction looks at a frozen moment of time. We can't even
// populate the cache because the data may be old.
return this.raw.get(txn, key);
}
else
{
// Must fetch as a collection to distinguish negative results.
Map<Key, Entity> map = this.getFromCacheRaw(Collections.singleton(key));
if (map.isEmpty())
{
try
{
Entity ent = this.raw.get(txn, key);
map.put(key, ent);
this.putInCache(map, cachedAnnotation.expirationSeconds());
return ent;
}
catch (EntityNotFoundException e)
{
// cache negative result
map.put(key, null);
this.putInCache(map, cachedAnnotation.expirationSeconds());
throw e;
}
}
else
{
Entity result = map.values().iterator().next();
if (result == null)
throw new EntityNotFoundException(key);
else
return result;
}
}
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#get(com.google.appengine.api.datastore.Transaction, java.lang.Iterable)
*/
@Override
public Map<Key, Entity> get(Transaction txn, Iterable<Key> keys)
{
if (txn != null)
{
// Must not populate the cache since we are looking at a frozen moment in time.
return this.raw.get(txn, keys);
}
else
{
// soFar will not containe uncacheables
Map<Key, Entity> soFar = this.getFromCache(keys);
Set<Key> stillNeeded = new HashSet<Key>();
for (Key getKey: keys)
if (!soFar.containsKey(getKey))
stillNeeded.add(getKey);
if (!stillNeeded.isEmpty())
{
// Includes negative results
Map<Key, Entity> fetched = this.getFromDatastore(txn, stillNeeded);
soFar.putAll(fetched);
this.putInCache(fetched);
}
// Strip out any negative results
Iterator<Entity> it = soFar.values().iterator();
while (it.hasNext())
if (it.next() == null)
it.remove();
return soFar;
}
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#getActiveTransactions()
*/
@Override
public Collection<Transaction> getActiveTransactions()
{
return this.raw.getActiveTransactions();
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#getCurrentTransaction()
*/
@Override
public Transaction getCurrentTransaction()
{
return this.raw.getCurrentTransaction();
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#getCurrentTransaction(com.google.appengine.api.datastore.Transaction)
*/
@Override
public Transaction getCurrentTransaction(Transaction txn)
{
return this.raw.getCurrentTransaction(txn);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#prepare(com.google.appengine.api.datastore.Query)
*/
@Override
public PreparedQuery prepare(Query query)
{
return this.raw.prepare(query);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#prepare(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Query)
*/
@Override
public PreparedQuery prepare(Transaction txn, Query query)
{
return this.raw.prepare(txn, query);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#put(com.google.appengine.api.datastore.Entity)
*/
@Override
public Key put(Entity entity)
{
return this.put(null, entity);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#put(java.lang.Iterable)
*/
@Override
public List<Key> put(Iterable<Entity> entities)
{
return this.put(null, entities);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#put(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Entity)
*/
@Override
public Key put(Transaction txn, Entity entity)
{
Key result = this.raw.put(txn, entity);
// Cacheability checking is handled inside these methods
if (txn != null)
((TransactionWrapper)txn).deferCachePut(entity);
else
this.putInCache(Collections.singletonMap(entity.getKey(), entity));
return result;
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#put(com.google.appengine.api.datastore.Transaction, java.lang.Iterable)
*/
@Override
public List<Key> put(Transaction txn, Iterable<Entity> entities)
{
List<Key> result = this.raw.put(txn, entities);
if (txn != null)
{
for (Entity ent: entities)
((TransactionWrapper)txn).deferCachePut(ent);
}
else
{
Map<Key, Entity> map = new HashMap<Key, Entity>();
for (Entity entity: entities)
map.put(entity.getKey(), entity);
this.putInCache(map);
}
return result;
}
}