package com.googlecode.objectify.cache;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreAttributes;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Index;
import com.google.appengine.api.datastore.Index.IndexState;
import com.google.appengine.api.datastore.Key;
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.datastore.TransactionOptions;
import com.googlecode.objectify.cache.EntityMemcache.Bucket;
import com.googlecode.objectify.util.FutureNow;
import com.googlecode.objectify.util.SimpleFutureWrapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>A write-through memcache for Entity objects that works for both transactional
* and nontransactional sessions.</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>
* <li>This cache has near-transactional integrity. As long as DeadlineExceededException is not hit, cache should
* not go out of sync even under heavy contention.</li>
* </ul>
*
* <p>Note: Until Google adds a hook that lets us wrap native Future<?> implementations,
* you muse install the {@code AsyncCacheFilter} to use this cache asynchronously. This
* is not necessary for synchronous use of {@code CachingDatastoreService}, but asynchronous
* operation requires an extra hook for the end of a request when fired-and-forgotten put()s
* and delete()s get processed. <strong>If you use this cache asynchronously, and you do not
* use the {@code AsyncCacheFilter}, your cache will go out of sync.</strong></p>
*
* @author Jeff Schnitzer <jeff@infohazard.org>
*/
public class CachingAsyncDatastoreService implements AsyncDatastoreService
{
private static final Logger log = Logger.getLogger(CachingAsyncDatastoreService.class.getName());
/** The real datastore service objects - we need both */
private AsyncDatastoreService rawAsync;
/** */
private EntityMemcache memcache;
/**
*/
public CachingAsyncDatastoreService(AsyncDatastoreService rawAsync, EntityMemcache memcache) {
this.rawAsync = rawAsync;
this.memcache = memcache;
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#allocateIds(java.lang.String, long)
*/
@Override
public Future<KeyRange> allocateIds(String kind, long num)
{
return this.rawAsync.allocateIds(kind, num);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#allocateIds(com.google.appengine.api.datastore.Key, java.lang.String, long)
*/
@Override
public Future<KeyRange> allocateIds(Key parent, String kind, long num)
{
return this.rawAsync.allocateIds(parent, kind, num);
}
/**
* Need this for beingTransaction()
*/
private class TransactionFutureWrapper extends SimpleFutureWrapper<Transaction, Transaction>
{
CachingTransaction xact;
public TransactionFutureWrapper(Future<Transaction> base)
{
super(base);
}
@Override
protected Transaction wrap(Transaction t)
{
if (xact == null)
xact = new CachingTransaction(memcache, t);
return xact;
}
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#beginTransaction()
*/
@Override
public Future<Transaction> beginTransaction()
{
return new TransactionFutureWrapper(this.rawAsync.beginTransaction());
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#beginTransaction(com.google.appengine.api.datastore.TransactionOptions)
*/
@Override
public Future<Transaction> beginTransaction(TransactionOptions options)
{
return new TransactionFutureWrapper(this.rawAsync.beginTransaction(options));
}
/**
* We don't allow implicit transactions, so throw an exception if the user is trying to use one.
*/
private void checkForImplicitTransaction()
{
if (this.rawAsync.getCurrentTransaction(null) != null)
throw new UnsupportedOperationException("Implicit, thread-local transactions are not supported by the cache. You must pass in an transaction (or null) explicitly.");
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#delete(com.google.appengine.api.datastore.Key[])
*/
@Override
public Future<Void> delete(Key... keys)
{
this.checkForImplicitTransaction();
return this.delete(null, keys);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#delete(java.lang.Iterable)
*/
@Override
public Future<Void> delete(Iterable<Key> keys)
{
this.checkForImplicitTransaction();
return this.delete(null, keys);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#delete(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Key[])
*/
@Override
public Future<Void> delete(Transaction txn, Key... keys)
{
return this.delete(txn, Arrays.asList(keys));
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#delete(com.google.appengine.api.datastore.Transaction, java.lang.Iterable)
*/
@Override
public Future<Void> delete(final Transaction txn, final Iterable<Key> keys)
{
// Always trigger, even on failure - the delete might have succeeded even though a timeout
// exception was thrown. We will always be safe emptying the key from the cache.
Future<Void> future = new TriggerFuture<Void>(this.rawAsync.delete(txn, keys)) {
@Override
protected void trigger()
{
if (txn != null)
{
for (Key key: keys)
((CachingTransaction)txn).deferEmptyFromCache(key);
}
else
{
memcache.empty(keys);
}
}
};
if (txn instanceof CachingTransaction)
((CachingTransaction)txn).enlist(future);
return future;
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#get(com.google.appengine.api.datastore.Key)
*/
@Override
public Future<Entity> get(Key key)
{
this.checkForImplicitTransaction();
return this.get(null, key);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#get(java.lang.Iterable)
*/
@Override
public Future<Map<Key, Entity>> get(Iterable<Key> keys)
{
this.checkForImplicitTransaction();
return this.get(null, keys);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#get(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Key)
*/
@Override
public Future<Entity> get(Transaction txn, final Key key)
{
Future<Map<Key, Entity>> bulk = this.get(txn, Collections.singleton(key));
return new SimpleFutureWrapper<Map<Key, Entity>, Entity>(bulk) {
@Override
protected Entity wrap(Map<Key, Entity> entities) throws Exception
{
Entity ent = entities.get(key);
if (ent == null)
throw new EntityNotFoundException(key);
else
return ent;
}
};
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#get(com.google.appengine.api.datastore.Transaction, java.lang.Iterable)
*/
@Override
public Future<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.rawAsync.get(txn, keys);
}
else
{
Map<Key, Bucket> soFar = this.memcache.getAll(keys);
final List<Bucket> uncached = new ArrayList<>(soFar.size());
Map<Key, Entity> cached = new HashMap<>();
for (Bucket buck: soFar.values())
if (buck.isEmpty())
uncached.add(buck);
else if (!buck.isNegative())
cached.put(buck.getKey(), buck.getEntity());
// Maybe we need to fetch some more
Future<Map<Key, Entity>> pending = null;
if (!uncached.isEmpty())
{
Future<Map<Key, Entity>> fromDatastore = this.rawAsync.get(null, EntityMemcache.keysOf(uncached));
pending = new TriggerSuccessFuture<Map<Key, Entity>>(fromDatastore) {
@Override
public void success(Map<Key, Entity> result)
{
for (Bucket buck: uncached)
{
Entity value = result.get(buck.getKey());
if (value != null)
buck.setNext(value);
}
memcache.putAll(uncached);
}
};
}
// If there was nothing from the cache, don't need to merge!
if (cached.isEmpty())
if (pending == null)
return new FutureNow<>(cached); // empty!
else
return pending;
else
return new MergeFuture<>(cached, pending);
}
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.BaseDatastoreService#getActiveTransactions()
*/
@Override
public Collection<Transaction> getActiveTransactions()
{
// This would conflict with the wrapped transaction object
throw new UnsupportedOperationException();
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.BaseDatastoreService#getCurrentTransaction()
*/
@Override
public Transaction getCurrentTransaction()
{
// This would conflict with the wrapped transaction object
throw new UnsupportedOperationException();
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.BaseDatastoreService#getCurrentTransaction(com.google.appengine.api.datastore.Transaction)
*/
@Override
public Transaction getCurrentTransaction(Transaction txn)
{
// This would conflict with the wrapped transaction object
throw new UnsupportedOperationException();
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.BaseDatastoreService#prepare(com.google.appengine.api.datastore.Query)
*/
@Override
public PreparedQuery prepare(Query query)
{
this.checkForImplicitTransaction();
return this.rawAsync.prepare(query);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.BaseDatastoreService#prepare(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Query)
*/
@Override
public PreparedQuery prepare(Transaction txn, Query query)
{
return this.rawAsync.prepare(txn, query);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#put(com.google.appengine.api.datastore.Entity)
*/
@Override
public Future<Key> put(Entity entity)
{
this.checkForImplicitTransaction();
return this.put(null, entity);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.DatastoreService#put(java.lang.Iterable)
*/
@Override
public Future<List<Key>> put(Iterable<Entity> entities)
{
this.checkForImplicitTransaction();
return this.put(null, entities);
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#put(com.google.appengine.api.datastore.Transaction, com.google.appengine.api.datastore.Entity)
*/
@Override
public Future<Key> put(final Transaction txn, final Entity entity)
{
Future<List<Key>> bulk = this.put(txn, Collections.singleton(entity));
return new SimpleFutureWrapper<List<Key>, Key>(bulk) {
@Override
protected Key wrap(List<Key> keys) throws Exception
{
return keys.get(0);
}
};
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#put(com.google.appengine.api.datastore.Transaction, java.lang.Iterable)
*/
@Override
public Future<List<Key>> put(final Transaction txn, final Iterable<Entity> entities)
{
// There is one weird case we have to watch out for. When you put() entities without
// a key, the backend autogenerates the key for you. But the put() might throw an
// exception (eg timeout) even though it succeeded in the backend. Thus we wrote
// an entity in the datastore but we don't know what the key was, so we can't empty
// out any negative cache entry that might exist.
// The solution to this is that we need to allocate ids ourself before put()ing the entity.
// Unfortunately there is no Entity.setKey() method or Key.setId() method, so we can't do this
// The best we can do is watch out for when there is a potential problem and warn the
// developer in the logs.
final List<Key> inputKeys = new ArrayList<>();
boolean foundAutoGenKeys = false;
for (Entity ent: entities)
if (ent.getKey() != null)
inputKeys.add(ent.getKey());
else
foundAutoGenKeys = true;
final boolean hasAutoGenKeys = foundAutoGenKeys;
// Always trigger, even on failure - the delete might have succeeded even though a timeout
// exception was thrown. We will always be safe emptying the key from the cache.
Future<List<Key>> future = new TriggerFuture<List<Key>>(this.rawAsync.put(txn, entities)) {
@Override
protected void trigger()
{
// This is complicated by the fact that some entities may have been put() without keys,
// so they will have been autogenerated in the backend. If a timeout error is thrown,
// it's possible the commit succeeded but we won't know what the key was. If there was
// already a negative cache entry for this, we have no way of knowing to clear it.
// This must be pretty rare: A timeout on a autogenerated key when there was already a
// negative cache entry. We can detect when this is a potential case and log a warning.
// The only real solution to this is to allocate ids in advance. Which maybe we should do.
List<Key> keys;
try {
keys = this.raw.get();
} catch (Exception ex) {
keys = inputKeys;
if (hasAutoGenKeys)
log.log(Level.WARNING, "A put() for an Entity with an autogenerated key threw an exception. Because the write" +
" might have succeeded and there might be a negative cache entry for the (generated) id, there" +
" is a small potential for cache to be incorrect.");
}
if (txn != null)
{
for (Key key: keys)
((CachingTransaction)txn).deferEmptyFromCache(key);
}
else
{
memcache.empty(keys);
}
}
};
if (txn instanceof CachingTransaction)
((CachingTransaction)txn).enlist(future);
return future;
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#getDatastoreAttributes()
*/
@Override
public Future<DatastoreAttributes> getDatastoreAttributes()
{
return this.rawAsync.getDatastoreAttributes();
}
/* (non-Javadoc)
* @see com.google.appengine.api.datastore.AsyncDatastoreService#getIndexes()
*/
@Override
public Future<Map<Index, IndexState>> getIndexes()
{
return this.rawAsync.getIndexes();
}
}