package com.googlecode.objectify.impl; import com.google.appengine.api.datastore.Entity; import com.googlecode.objectify.Key; import com.googlecode.objectify.Ref; import com.googlecode.objectify.Result; import com.googlecode.objectify.impl.translate.SaveContext; import com.googlecode.objectify.util.ResultCache; import com.googlecode.objectify.util.ResultNow; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Each round in the series of fetches required to complete a batch. A round executes when * the value is obtained (via now()) for a Result that was created as part of this round. * When a round executes, a new round is created. */ class Round { /** */ private static final Logger log = Logger.getLogger(Round.class.getName()); /** */ private final LoadEngine loadEngine; /** The depth of the rounds of execution, for debugging. 0 is first round, 1 is second round, etc */ private final int depth; /** The keys we will need to fetch; might not be any if everything came from the session */ private final Set<com.google.appengine.api.datastore.Key> pending = new HashSet<>(); /** Sometimes we get a bunch of Entity data from queries that eliminates our need to go to the backing datastore */ private final Map<com.google.appengine.api.datastore.Key, Entity> stuffed = new HashMap<>(); /** Entities that have been fetched and translated this round. There will be an entry for each pending. */ Result<Map<Key<?>, Object>> translated; /** */ Round(LoadEngine loadEngine, int depth) { this.loadEngine = loadEngine; this.depth = depth; } /** * Gets a result, using the session cache if possible. */ public <T> Result<T> get(final Key<T> key) { assert !isExecuted(); SessionValue<T> sv = getSession().get(key); if (sv == null) { if (log.isLoggable(Level.FINEST)) log.finest("Adding to round (session miss): " + key); this.pending.add(key.getRaw()); Result<T> result = new ResultCache<T>() { @Override @SuppressWarnings("unchecked") public T nowUncached() { // Because clients could conceivably get() in the middle of our operations (see LoadCollectionRefsTest.specialListWorks()), // we need to check for early execution. This will perform poorly, but at least it will work. //assert Round.this.isExecuted(); loadEngine.execute(); return (T)translated.now().get(key); } @Override public String toString() { return "(Fetch result for " + key + ")"; } }; sv = new SessionValue<>(result, getLoadArrangement()); getSession().add(key, sv); } else { if (log.isLoggable(Level.FINEST)) log.finest("Adding to round (session hit): " + key); if (sv.loadWith(getLoadArrangement())) { if (log.isLoggable(Level.FINEST)) log.finest("New load group arrangement, checking for upgrades: " + getLoadArrangement()); // We are looking at a brand-new arrangement for something that already existed in the session. // We need to go through any Ref<?>s that might be in need of loading. We find those refs by // actually saving the entity into a custom SaveContext. T thing = sv.getResult().now(); if (thing != null) { SaveContext saveCtx = new SaveContext() { @Override public boolean skipLifecycle() { return true; } @Override public com.google.appengine.api.datastore.Key saveRef(Ref<?> value, LoadConditions loadConditions) { com.google.appengine.api.datastore.Key key = super.saveRef(value, loadConditions); if (loadEngine.shouldLoad(loadConditions)) { if (log.isLoggable(Level.FINEST)) log.finest("Upgrading key " + key); loadEngine.load(value.key()); } return key; } }; // We throw away the saved entity and we are done loadEngine.ofy.factory().getMetadataForEntity(thing).save(thing, saveCtx); } } } return sv.getResult(); } /** @return true if this round needs execution */ public boolean needsExecution() { return translated == null && !pending.isEmpty(); } /** Turn this into a result set */ public void execute() { if (needsExecution()) { if (log.isLoggable(Level.FINEST)) log.finest("Executing round: " + pending); Result<Map<com.google.appengine.api.datastore.Key, Entity>> fetched = fetchPending(); translated = loadEngine.translate(fetched); // If we're in a transaction (and beyond the first round), force all subsequent rounds to complete. // This effectively means that only the first round can be asynchronous; all other rounds are // materialized immediately. The reason for this is that there are some nasty edge cases with @Load // annotations in transactions getting called after the transaction closes. This is possibly not the // best solution to the problem, but it solves the problem now. if (loadEngine.ofy.getTransaction() != null && depth > 0) translated.now(); } } /** Possibly pulls some values from the stuffed collection */ private Result<Map<com.google.appengine.api.datastore.Key, Entity>> fetchPending() { // We don't need to fetch anything that has been stuffed final Map<com.google.appengine.api.datastore.Key, Entity> combined = new HashMap<>(); Set<com.google.appengine.api.datastore.Key> fetch = new HashSet<>(); for (com.google.appengine.api.datastore.Key key: pending) { Entity ent = stuffed.get(key); if (ent == null) fetch.add(key); else combined.put(key, ent); } if (fetch.isEmpty()) { return new ResultNow<>(combined); } else { final Result<Map<com.google.appengine.api.datastore.Key, Entity>> fetched = loadEngine.fetch(fetch); return new Result<Map<com.google.appengine.api.datastore.Key, Entity>>() { @Override public Map<com.google.appengine.api.datastore.Key, Entity> now() { combined.putAll(fetched.now()); return combined; } }; } } /** */ @Override public String toString() { return (isExecuted() ? "pending" : "executed") + ", depth=" + depth + ", pending="+ pending.toString(); } /** * @return true if the round has been executed already */ public boolean isExecuted() { return translated != null; } /** Create the next round */ public Round next() { if (log.isLoggable(Level.FINEST)) log.finest("Creating new round, going from depth " + depth + " to " + (depth+1)); return new Round(loadEngine, depth+1); } /** * Stuffs an Entity into a place where values in the round can be obtained instead of going to the datastore. * Called by non-hybrid queries to add results and eliminate batch fetching. */ public void stuff(Entity ent) { stuffed.put(ent.getKey(), ent); } /** */ private Session getSession() { return loadEngine.getSession(); } /** */ private LoadArrangement getLoadArrangement() { return loadEngine.getLoadArrangement(); } }