All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.tools.remoteapi.RemoteDatastore Maven / Gradle / Ivy

/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.appengine.tools.remoteapi;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.datastore.DatastoreV3Pb;
import com.google.io.protocol.ProtocolMessage;
import com.google.storage.onestore.v3.OnestoreEntity;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Contains handlers for calling the datastore via the remote API.
 *
 * 

A note on Reference re-writing: * *

This class needs to handle Keys containing app ids of either the client app or the remote app. * Apps that are using the latest version of the Remote API will generate Keys with the remote app * id after installing the Remote API. However, older versions of the Remote API will generate Keys * with the client app id. Additionally, it's possible that the app could create a Key prior to * installing the Remote API and use that. * *

This class makes sure that all Keys in requests sent to the remote app have the remote app id. * Keys in responses will also have the remote app id. * *

However, due to implementation details of the Java SDK, Keys from Put operations end up being * returned to the user matching the app id of the request. That is, they will match the app id that * was on the Entity being Put. As discussed above, this could be either the client app id or the * remote app id. TODO: consider updating this. */ class RemoteDatastore { static final String DATASTORE_SERVICE = "datastore_v3"; static final String REMOTE_API_SERVICE = "remote_datastore"; private static final Logger logger = Logger.getLogger(RemoteDatastore.class.getName()); private final RemoteRpc remoteRpc; private final RemoteApiOptions options; private final String remoteAppId; /** Contains an entry for every query we've ever run. */ private final Map idToCursor = new ConcurrentHashMap<>(); // TODO(b/68190107) entries are never removed, which is a memory leak. // (But Python has the same problem.) /** * A counter used to allocate local cursor ids. */ private final AtomicLong nextCursorId = new AtomicLong(1); /** Contains an entry for each in-progress transaction. */ private final Map idToTransaction = new ConcurrentHashMap<>(); /** * A counter used to allocate transaction ids. */ private final AtomicLong nextTransactionId = new AtomicLong(1); RemoteDatastore(String remoteAppId, RemoteRpc remoteRpc, RemoteApiOptions options) { this.remoteAppId = remoteAppId; this.remoteRpc = remoteRpc; this.options = options; } byte[] handleDatastoreCall(String methodName, byte[] request) { // TODO(b/68190109) Perhaps replace with a map of handlers. // TODO Support AddActions. switch (methodName) { case "RunQuery": return handleRunQuery(request); case "Next": return handleNext(request); case "BeginTransaction": return handleBeginTransaction(request); case "Commit": return handleCommit(request); case "Rollback": return handleRollback(request); case "Get": return handleGet(request); case "Put": return handlePut(request); case "Delete": return handleDelete(request); default: // other datastore call return remoteRpc.call(DATASTORE_SERVICE, methodName, "", request); } } private byte[] handleRunQuery(byte[] request) { return runQuery(request, nextCursorId.getAndIncrement()); } /** * Runs the query and remembers the current position using the given cursor id. */ private byte[] runQuery(byte[] request, long localCursorId) { // force query compilation so we get a compiled cursor back DatastoreV3Pb.Query query = new DatastoreV3Pb.Query(); mergeFromBytes(query, request); if (rewriteQueryAppIds(query, remoteAppId)) { request = query.toByteArray(); } query.setCompile(true); // override fetch size; the normal default is too small for the remote API. if (!query.hasCount()) { query.setCount(options.getDatastoreQueryFetchSize()); } // Force the query not to run in a transaction. Transactions // cannot span multiple remote calls, because each call // might happen on a different server. The actual transaction // (if any) won't happen until commit. Instead we will record // the results of the query and verify that they haven't changed // at commit time. TransactionBuilder tx = null; if (query.hasTransaction()) { tx = getTransactionBuilder("RunQuery", query.getTransaction()); query.clearTransaction(); } DatastoreV3Pb.QueryResult result; if (tx != null) { byte[] resultBytes = remoteRpc.call(REMOTE_API_SERVICE, "TransactionQuery", "", query.toByteArray()); result = tx.handleQueryResult(resultBytes); } else { byte[] resultBytes = remoteRpc.call(DATASTORE_SERVICE, "RunQuery", "", query.toByteArray()); result = new DatastoreV3Pb.QueryResult(); mergeFromBytes(result, resultBytes); if (tx != null) { // Add the preconditions for this query result. The assertion is that // none of the entities that we actually downloaded as a result of the // query have changed. (This is probably a performance hit for a query // that returns many results since we'll be doing a bulk get at commit // time.) // This consistency check will not detect "phantom" rows. If we wanted // that, we would have to change the remote API protocol so that we can // re-run all the queries in the transaction at commit time. for (OnestoreEntity.EntityProto entity : result.results()) { tx.addEntityToCache(entity); } } } if (result.isMoreResults() && result.hasCompiledCursor()) { // create a query to continue from after the results we already got. idToCursor.put(localCursorId, new QueryState(request, result.getCompiledCursor())); } else { idToCursor.put(localCursorId, QueryState.NO_MORE_RESULTS); } // replace cursor id with our own cursor, to be used in handleNext() to look up the query. result.getMutableCursor().setCursor(localCursorId); return result.toByteArray(); } /** * Rewrite app ids in the Query pb. * @return if any app ids were rewritten */ /* @VisibleForTesting */ static boolean rewriteQueryAppIds(DatastoreV3Pb.Query query, String remoteAppId) { boolean reserialize = false; if (!query.getApp().equals(remoteAppId)) { reserialize = true; query.setApp(remoteAppId); } if (query.hasAncestor() && !query.getAncestor().getApp().equals(remoteAppId)) { reserialize = true; query.getAncestor().setApp(remoteAppId); } for (DatastoreV3Pb.Query.Filter filter : query.filters()) { for (OnestoreEntity.Property prop : filter.propertys()) { OnestoreEntity.PropertyValue propValue = prop.getMutableValue(); if (propValue.hasReferenceValue()) { OnestoreEntity.PropertyValue.ReferenceValue ref = propValue.getMutableReferenceValue(); if (!ref.getApp().equals(remoteAppId)) { reserialize = true; ref.setApp(remoteAppId); } } } } return reserialize; } private byte[] handleNext(byte[] request) { DatastoreV3Pb.NextRequest nextRequest = new DatastoreV3Pb.NextRequest(); mergeFromBytes(nextRequest, request); long cursorId = nextRequest.getCursor().getCursor(); QueryState queryState = idToCursor.get(cursorId); if (queryState == null) { throw new RemoteApiException("local cursor not found", DATASTORE_SERVICE, "Next", null); } if (!queryState.hasMoreResults()) { DatastoreV3Pb.QueryResult result = new DatastoreV3Pb.QueryResult(); result.setMoreResults(false); return result.toByteArray(); } else { return runQuery(queryState.makeNextQuery(nextRequest).toByteArray(), cursorId); } } private byte[] handleBeginTransaction(byte[] request) { DatastoreV3Pb.BeginTransactionRequest beginTxnRequest = new DatastoreV3Pb.BeginTransactionRequest(); parseFromBytes(beginTxnRequest, request); // Create the transaction builder. long txId = nextTransactionId.getAndIncrement(); idToTransaction.put(txId, new TransactionBuilder(beginTxnRequest.isAllowMultipleEg())); // return a Transaction response with the new id DatastoreV3Pb.Transaction tx = new DatastoreV3Pb.Transaction(); tx.setHandle(txId); tx.setApp(remoteAppId); return tx.toByteArray(); } private byte[] handleCommit(byte[] requestBytes) { DatastoreV3Pb.Transaction request = new DatastoreV3Pb.Transaction(); mergeFromBytes(request, requestBytes); request.setApp(remoteAppId); TransactionBuilder tx = removeTransactionBuilder("Commit", request); // Replay the transaction and do the commit on the server. (Throws an exception // if the commit fails.) remoteRpc.call(REMOTE_API_SERVICE, "Transaction", "", tx.makeCommitRequest().toByteArray()); // Return success. return new DatastoreV3Pb.CommitResponse().toByteArray(); } private byte[] handleRollback(byte[] requestBytes) { DatastoreV3Pb.Transaction request = new DatastoreV3Pb.Transaction(); mergeFromBytes(request, requestBytes); request.setApp(remoteAppId); removeTransactionBuilder("Rollback", request); return new byte[0]; // this is ApiBasePb.VoidProto.getDefaultInstance().toByteArray(); } private byte[] handleGet(byte[] originalRequestBytes) { DatastoreV3Pb.GetRequest rewrittenReq = new DatastoreV3Pb.GetRequest(); mergeFromBytes(rewrittenReq, originalRequestBytes); // Update the Request so that all References have the remoteAppId. boolean reserialize = rewriteRequestReferences(rewrittenReq.mutableKeys(), remoteAppId); if (rewrittenReq.hasTransaction()) { return handleGetWithTransaction(rewrittenReq); } else { // Send the rpc. byte[] requestBytesToSend = reserialize ? rewrittenReq.toByteArray() : originalRequestBytes; return remoteRpc.call(DATASTORE_SERVICE, "Get", "", requestBytesToSend); } } private byte[] handlePut(byte[] requestBytes) { DatastoreV3Pb.PutRequest request = new DatastoreV3Pb.PutRequest(); mergeFromBytes(request, requestBytes); boolean reserialize = rewritePutAppIds(request, remoteAppId); if (request.hasTransaction()) { return handlePutForTransaction(request); } else { if (reserialize) { requestBytes = request.toByteArray(); } String suffix = ""; if (logger.isLoggable(Level.FINE)) { // Log the key of the first entity for put calls that happen outside a transaction. suffix = describePutRequestForLog(request); } return remoteRpc.call(DATASTORE_SERVICE, "Put", suffix, requestBytes); } } /* @VisibleForTesting */ static boolean rewritePutAppIds(DatastoreV3Pb.PutRequest request, String remoteAppId) { boolean reserialize = false; // rewrite the app on the key of every entity for (OnestoreEntity.EntityProto entity : request.mutableEntitys()) { if (!entity.getMutableKey().getApp().equals(remoteAppId)) { reserialize = true; entity.getMutableKey().setApp(remoteAppId); } // rewrite the app on all reference properties for (OnestoreEntity.Property prop : entity.mutablePropertys()) { if (prop.hasValue() && prop.getMutableValue().hasReferenceValue()) { OnestoreEntity.PropertyValue.ReferenceValue ref = prop.getMutableValue().getReferenceValue(); if (ref.hasApp() && !ref.getApp().equals(remoteAppId)) { reserialize = true; ref.setApp(remoteAppId); } } } } return reserialize; } private byte[] handleDelete(byte[] requestBytes) { DatastoreV3Pb.DeleteRequest request = new DatastoreV3Pb.DeleteRequest(); mergeFromBytes(request, requestBytes); boolean reserialize = rewriteRequestReferences(request.mutableKeys(), remoteAppId); if (reserialize) { // The request was mutated, so we need to reserialize it. requestBytes = request.toByteArray(); } if (request.hasTransaction()) { return handleDeleteForTransaction(request); } else { return remoteRpc.call(DATASTORE_SERVICE, "Delete", "", requestBytes); } } /** * Replace app ids in the collection of references. * * @param references The references that may need to be updated. If so, they will be mutated in * place. * @param remoteAppId The app id that should be present on the references. * @return A boolean indicating if any changes were made. */ /* @VisibleForTesting */ static boolean rewriteRequestReferences( Collection references, String remoteAppId) { boolean reserialize = false; for (OnestoreEntity.Reference refToCheck : references) { if (!refToCheck.getApp().equals(remoteAppId)) { refToCheck.setApp(remoteAppId); reserialize = true; } } return reserialize; } private byte[] handleGetWithTransaction(DatastoreV3Pb.GetRequest rewrittenReq) { TransactionBuilder tx = getTransactionBuilder("Get", rewrittenReq.getTransaction()); // We only send a request for keys that are not already in the cache. Note that the // References have already been rewritten to have the remoteAppId. Also note that this request // does not actually use a transaction. Instead, all transactional checks will be done at // commit time. DatastoreV3Pb.GetRequest requestForKeysNotInCache = rewrittenReq.clone(); requestForKeysNotInCache.clearTransaction(); requestForKeysNotInCache.clearKey(); for (OnestoreEntity.Reference key : rewrittenReq.keys()) { if (!tx.isCachedEntity(key)) { requestForKeysNotInCache.addKey(key); } } // If we need any entities, do the RPC Set deferredRefs = new HashSet<>(); if (requestForKeysNotInCache.keySize() > 0) { byte[] respBytesFromRemoteApp = remoteRpc.call( RemoteDatastore.DATASTORE_SERVICE, "Get", "", requestForKeysNotInCache.toByteArray()); // Add new entities to the cache (these have the remote app id.) DatastoreV3Pb.GetResponse respFromRemoteApp = new DatastoreV3Pb.GetResponse(); mergeFromBytes(respFromRemoteApp, respBytesFromRemoteApp); for (DatastoreV3Pb.GetResponse.Entity entityResult : respFromRemoteApp.entitys()) { if (entityResult.hasEntity()) { tx.addEntityToCache(entityResult.getEntity()); } else { tx.addEntityAbsenceToCache(entityResult.getKey()); } } // We don't update the cache for deferred Keys, but we'll make sure they flow back out // through the returned GetResponse. deferredRefs.addAll(respFromRemoteApp.deferreds()); } // The cache is now up to date. We'll build the response by pulling values from it. DatastoreV3Pb.GetResponse mergedResponse = new DatastoreV3Pb.GetResponse(); mergedResponse.setInOrder(deferredRefs.isEmpty()); for (OnestoreEntity.Reference key : rewrittenReq.keys()) { // Check for deferred keys first, because they were not put in the cache. if (deferredRefs.contains(key)) { mergedResponse.addDeferred(key); } else { // Otherwise, it should be in the cache (perhaps as a MISSING entry.) OnestoreEntity.EntityProto entity = tx.getCachedEntity(key); if (entity == null) { mergedResponse.addEntity().setKey(key); } else { mergedResponse.addEntity().setEntity(entity); } } } return mergedResponse.toByteArray(); } byte[] handlePutForTransaction(DatastoreV3Pb.PutRequest request) { TransactionBuilder tx = getTransactionBuilder("Put", request.getTransaction()); // Find the entities for which we need to allocate a new id. List entitiesWithoutIds = new ArrayList<>(); for (OnestoreEntity.EntityProto entity : request.entitys()) { if (requiresId(entity)) { entitiesWithoutIds.add(entity); } } // Allocate an id for each entity that needs one. if (!entitiesWithoutIds.isEmpty()) { DatastoreV3Pb.PutRequest subRequest = new DatastoreV3Pb.PutRequest(); for (OnestoreEntity.EntityProto entity : entitiesWithoutIds) { OnestoreEntity.EntityProto subEntity = subRequest.addEntity(); subEntity.getKey().mergeFrom(entity.getKey()); subEntity.getEntityGroup(); } // Gross, but there's no place to hide this attribute in the proto we // send over so we just use a separate RPC. If we end up with more // txn options we'll need to come up with something else. String getIdsRpc = tx.isXG() ? "GetIDsXG" : "GetIDs"; byte[] subResponseBytes = remoteRpc.call(REMOTE_API_SERVICE, getIdsRpc, "", subRequest.toByteArray()); DatastoreV3Pb.PutResponse subResponse = new DatastoreV3Pb.PutResponse(); mergeFromBytes(subResponse, subResponseBytes); // Add the new id and its entity group to the original entity (still in the request). Iterator it = entitiesWithoutIds.iterator(); for (OnestoreEntity.Reference newKey : subResponse.keys()) { OnestoreEntity.EntityProto entity = it.next(); entity.getKey().copyFrom(newKey); entity.getEntityGroup().addElement().copyFrom(newKey.getPath().getElement(0)); } } // Copy all the entities in this put() request into the transaction, to be submitted // to the server on commit. Also, create a response that has the key of each entity. DatastoreV3Pb.PutResponse response = new DatastoreV3Pb.PutResponse(); for (OnestoreEntity.EntityProto entityProto : request.entitys()) { tx.putEntityOnCommit(entityProto); response.addKey().copyFrom(entityProto.getKey()); } return response.toByteArray(); } byte[] handleDeleteForTransaction(DatastoreV3Pb.DeleteRequest request) { TransactionBuilder tx = getTransactionBuilder("Delete", request.getTransaction()); for (OnestoreEntity.Reference key : request.keys()) { tx.deleteEntityOnCommit(key); } DatastoreV3Pb.DeleteResponse response = new DatastoreV3Pb.DeleteResponse(); return response.toByteArray(); } TransactionBuilder getTransactionBuilder(String methodName, DatastoreV3Pb.Transaction tx) { TransactionBuilder result = idToTransaction.get(tx.getHandle()); if (result == null) { throw new RemoteApiException("transaction not found", DATASTORE_SERVICE, methodName, null); } return result; } TransactionBuilder removeTransactionBuilder(String methodName, DatastoreV3Pb.Transaction tx) { TransactionBuilder result = idToTransaction.remove(tx.getHandle()); if (result == null) { throw new RemoteApiException("transaction not found", DATASTORE_SERVICE, methodName, null); } return result; } /** * Returns true if we need to auto-allocate an id for this entity. */ private boolean requiresId(OnestoreEntity.EntityProto entity) { OnestoreEntity.Path path = entity.getKey().getPath(); OnestoreEntity.Path.Element lastElement = path.elements().get(path.elementSize() - 1); return lastElement.getId() == 0 && !lastElement.hasName(); } private static String describePutRequestForLog(DatastoreV3Pb.PutRequest putRequest) { int count = putRequest.entitySize(); if (count <= 0) { return "()"; } OnestoreEntity.Reference keyProto = putRequest.getEntity(0).getKey(); if (count == 1) { return "(" + describeKeyForLog(keyProto) + ")"; } else { return "(" + describeKeyForLog(keyProto) + ", ...)"; } } private static String describeKeyForLog(OnestoreEntity.Reference keyProto) { StringBuilder pathString = new StringBuilder(); OnestoreEntity.Path path = keyProto.getPath(); for (OnestoreEntity.Path.Element element : path.elements()) { if (pathString.length() > 0) { pathString.append(","); } pathString.append(element.getType() + "/"); if (element.hasId()) { pathString.append(element.getId()); } else { pathString.append(element.getName()); } } return "[" + pathString + "]"; } /** * The current state of a remote query, allowing us to continue from previous * location. (We need to keep this locally because each round trip can be * executed on a different instance.) */ private static class QueryState { private static final QueryState NO_MORE_RESULTS = new QueryState(null, null); private final byte[] query; private final DatastoreV3Pb.CompiledCursor cursor; /** * Creates a QueryState that can continue fetching results from a given cursor. * @param query the query that was previously executed * @param cursor the cursor that was returned after the previous remote call */ QueryState(byte[] query, DatastoreV3Pb.CompiledCursor cursor) { this.query = query; this.cursor = cursor; } boolean hasMoreResults() { return query != null; } private DatastoreV3Pb.Query makeNextQuery(DatastoreV3Pb.NextRequest nextRequest) { DatastoreV3Pb.Query result = new DatastoreV3Pb.Query(); mergeFromBytes(result, query); result.setOffset(0); result.setCompiledCursor(cursor); result.setCompile(true); if (nextRequest.hasCount()) { result.setCount(nextRequest.getCount()); } else { result.clearCount(); } return result; } } private static void parseFromBytes(ProtocolMessage message, byte[] bytes) { boolean parsed = message.parseFrom(bytes); if (!parsed || !message.isInitialized()) { throw new ApiProxy.ApiProxyException("Could not parse protobuf bytes"); } } private static void mergeFromBytes(ProtocolMessage message, byte[] bytes) { boolean parsed = message.mergeFrom(bytes); if (!parsed || !message.isInitialized()) { throw new ApiProxy.ApiProxyException("Could not parse protobuf bytes"); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy