com.google.appengine.api.datastore.TransactionImpl 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.api.datastore;
import com.google.appengine.api.utils.FutureWrapper;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* State and behavior that is common to all {@link Transaction} implementations.
*
* Our implementation is implicitly async. BeginTransaction RPCs always return instantly, and
* this class maintains a reference to the {@link Future} associated with the RPC. We service as
* much of the {@link Transaction} interface as we can without retrieving the result of the future.
*
*
There is no synchronization in this code because transactions are associated with a single
* thread and are documented as such.
*/
class TransactionImpl implements Transaction, CurrentTransactionProvider {
private static final Logger logger = Logger.getLogger(TransactionImpl.class.getName());
/**
* Interface to a coupled object which handles the actual transaction RPCs and other service
* protocol dependent details.
*/
interface InternalTransaction {
/** Issues an asynchronous RPC to commit this transaction. */
Future doCommitAsync();
/** Issues an asynchronous RPC to rollback this transaction. */
Future doRollbackAsync();
String getId();
@Override
boolean equals(@Nullable Object o);
@Override
int hashCode();
}
// The states in which a Transaction can exist.
enum TransactionState {
BEGUN,
// Used to detect calls to commit, commitAsync, rollback, and rollbackAsync
// while a commitAsync or a rollbackAsync is in progress.
COMPLETION_IN_PROGRESS,
COMMITTED,
ROLLED_BACK,
ERROR
}
private final String app;
private final TransactionStack txnStack;
private final DatastoreCallbacks callbacks;
private final boolean isExplicit;
private final InternalTransaction internalTransaction;
TransactionState state = TransactionState.BEGUN;
/** A {@link PostOpFuture} implementation that runs both post put and post delete callbacks. */
private class PostCommitFuture extends PostOpFuture {
private final List putEntities;
private final List deletedKeys;
private PostCommitFuture(
List putEntities, List deletedKeys, Future delegate) {
super(delegate, callbacks);
this.putEntities = putEntities;
this.deletedKeys = deletedKeys;
}
@Override
void executeCallbacks(Void ignoreMe) {
PutContext putContext = new PutContext(TransactionImpl.this, putEntities);
callbacks.executePostPutCallbacks(putContext);
DeleteContext deleteContext = new DeleteContext(TransactionImpl.this, deletedKeys);
callbacks.executePostDeleteCallbacks(deleteContext);
}
}
TransactionImpl(
String app,
TransactionStack txnStack,
DatastoreCallbacks callbacks,
boolean isExplicit,
InternalTransaction txnProvider) {
this.app = app;
this.txnStack = txnStack;
this.callbacks = callbacks;
this.isExplicit = isExplicit;
this.internalTransaction = txnProvider;
}
@Override
public String getId() {
return internalTransaction.getId();
}
@Override
public boolean equals(@Nullable Object o) {
if (o instanceof TransactionImpl) {
return internalTransaction.equals(((TransactionImpl) o).internalTransaction);
}
return false;
}
@Override
public int hashCode() {
return internalTransaction.hashCode();
}
@Override
public void commit() {
FutureHelper.quietGet(commitAsync());
}
@Override
public Future commitAsync() {
ensureTxnActive(this);
try {
// Make sure we wait for all dependent futures to finish first.
List exceptions = new ArrayList<>();
for (Future> f : txnStack.getFutures(this)) {
try {
FutureHelper.quietGet(f);
} catch (RuntimeException e) {
exceptions.add(e);
}
}
if (!exceptions.isEmpty()) {
// We'll throw the first exception and log any others.
for (int i = 1; i < exceptions.size(); i++) {
RuntimeException e = exceptions.get(i);
logger.log(Level.WARNING, "Failure while waiting to commit", e);
}
throw exceptions.get(0);
}
// Issue the commit RPC.
Future commitResponse = internalTransaction.doCommitAsync();
// Don't transition into the next state until we've successfully fired off
// the RPC. See http://b/5403846
state = TransactionState.COMPLETION_IN_PROGRESS;
// Translate the commit response.
Future result =
new FutureWrapper(commitResponse) {
@Override
protected Void wrap(Void ignore) throws Exception {
state = TransactionState.COMMITTED;
return null;
}
@Override
protected Throwable convertException(Throwable cause) {
// All exceptions force a transition to the error state.
state = TransactionState.ERROR;
return cause;
}
};
// Make sure we capture the entities and keys associated with this txn now
// because we're about to pop the stack and lose track of them.
return new PostCommitFuture(
txnStack.getPutEntities(this), txnStack.getDeletedKeys(this), result);
} finally {
// We might not be the current transaction so we have to request explicit
// removal. Note that we're doing this synchronously, so when you commit
// a txn, even async, the txn immediately ceases to be the current txn.
if (isExplicit) {
txnStack.remove(this);
}
}
}
@Override
public void rollback() {
FutureHelper.quietGet(rollbackAsync());
}
@Override
public Future rollbackAsync() {
ensureTxnActive(this);
try {
// Make sure we wait for all dependent futures to finish first. This
// is necessary to prevent server-side txns from being rolled back in the
// middle of transactional operations, a scenario that is almost certain
// to result in unexpected behavior.
for (Future> f : txnStack.getFutures(this)) {
try {
FutureHelper.quietGet(f);
} catch (RuntimeException e) {
// Any failure of these futures is irrelevant since we are rolling back.
// However, they may still be of interest for debugging.
logger.log(Level.INFO, "Failure while waiting to rollback", e);
}
}
Future future = internalTransaction.doRollbackAsync();
// Don't transition into the next state until we've successfully fired off
// the RPC. See http://b/5403846
state = TransactionState.COMPLETION_IN_PROGRESS;
return new FutureWrapper(future) {
@Override
protected Void wrap(Void ignore) throws Exception {
state = TransactionState.ROLLED_BACK;
return null;
}
@Override
protected Void absorbParentException(Throwable cause) throws Throwable {
// This will be executed if the rollback fails. We suppress the
// exception in that case so that callers don't have to worry about
// wrapping calls to rollback() in a try block.
logger.log(Level.INFO, "Rollback of transaction failed", cause);
state = TransactionState.ERROR;
return null;
}
@Override
protected Throwable convertException(Throwable cause) {
// It looks like this will never be executed, except possibly for
// something like an OutOfMemoryError. All expected exceptions
// are handled by absorbParentException.
state = TransactionState.ERROR;
// This throwable will be propagated.
return cause;
}
};
} finally {
// We might not be the current transaction so we have to request explicit
// removal. Note that we're doing this synchronously, so when you
// rollback a txn, even async, the txn immediately ceases to be the
// current txn.
if (isExplicit) {
txnStack.remove(this);
}
}
}
@Override
public String getApp() {
return app;
}
@Override
public boolean isActive() {
return state == TransactionState.BEGUN;
}
@Override
public Transaction getCurrentTransaction(Transaction defaultValue) {
return this;
}
/** If {@code txn} is not null and not active, throw {@link IllegalStateException}. */
static void ensureTxnActive(Transaction txn) {
if (txn != null && !txn.isActive()) {
throw new IllegalStateException(
"Transaction with which this operation is " + "associated is not active.");
}
}
@Override
public String toString() {
return "Txn [" + app + "." + getId() + ", " + state + "]";
}
}