Package com.googlecode.objectify.cache

Source Code of com.googlecode.objectify.cache.CachingAsyncDatastoreService

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();
  }
}


TOP

Related Classes of com.googlecode.objectify.cache.CachingAsyncDatastoreService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.