com.google.cloud.spanner.connection.ReadWriteTransaction Maven / Gradle / Ivy
Show all versions of google-cloud-spanner Show documentation
/*
* Copyright 2019 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
*
* http://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.cloud.spanner.connection;
import static com.google.cloud.spanner.SpannerApiFutures.get;
import static com.google.cloud.spanner.connection.AbstractStatementParser.BEGIN_STATEMENT;
import static com.google.cloud.spanner.connection.AbstractStatementParser.COMMIT_STATEMENT;
import static com.google.cloud.spanner.connection.AbstractStatementParser.ROLLBACK_STATEMENT;
import static com.google.cloud.spanner.connection.AbstractStatementParser.RUN_BATCH_STATEMENT;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.api.core.SettableApiFuture;
import com.google.cloud.Timestamp;
import com.google.cloud.Tuple;
import com.google.cloud.spanner.AbortedDueToConcurrentModificationException;
import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.Options;
import com.google.cloud.spanner.Options.QueryOption;
import com.google.cloud.spanner.Options.TransactionOption;
import com.google.cloud.spanner.Options.UpdateOption;
import com.google.cloud.spanner.ProtobufResultSet;
import com.google.cloud.spanner.ReadContext;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TransactionContext;
import com.google.cloud.spanner.TransactionManager;
import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
import com.google.cloud.spanner.connection.TransactionRetryListener.RetryResult;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.spanner.v1.SpannerGrpc;
import io.opentelemetry.context.Scope;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Transaction that is used when a {@link Connection} is normal read/write mode (i.e. not autocommit
* and not read-only). These transactions can be automatically retried if an {@link
* AbortedException} is thrown. The transaction will keep track of a running checksum of all {@link
* ResultSet}s that have been returned, and the update counts returned by any DML statement executed
* during the transaction. As long as these checksums and update counts are equal for both the
* original transaction and the retried transaction, the retry can safely be assumed to have the
* exact same results as the original transaction.
*/
class ReadWriteTransaction extends AbstractMultiUseTransaction {
private static final Logger logger = Logger.getLogger(ReadWriteTransaction.class.getName());
private static final AtomicLong ID_GENERATOR = new AtomicLong();
private static final String MAX_INTERNAL_RETRIES_EXCEEDED =
"Internal transaction retry maximum exceeded";
private static final int DEFAULT_MAX_INTERNAL_RETRIES = 50;
/**
* A reference to the currently active transaction on the emulator that was started by the same
* thread. This reference is only used when running on the emulator, and enables the Connection
* API to manually abort the current transaction on the emulator, so other transactions can try to
* make progress.
*/
private static final ThreadLocal CURRENT_ACTIVE_TRANSACTION =
new ThreadLocal<>();
/**
* The name of the automatic savepoint that is generated by the Connection API if automatically
* aborting the current active transaction on the emulator is enabled.
*/
private static final String AUTO_SAVEPOINT_NAME = "_auto_savepoint";
/**
* Indicates whether an automatic savepoint should be generated after each statement, so the
* transaction can be manually aborted and retried by the Connection API when connected to the
* emulator. This feature is only intended for use with the Spanner emulator. When connected to
* real Spanner, the decision whether to abort a transaction or not should be delegated to
* Spanner.
*/
private final boolean useAutoSavepointsForEmulator;
/**
* The savepoint that was automatically generated after executing the last statement. This is used
* to abort transactions on the emulator, if one thread tries to execute concurrent transactions
* on the emulator, and would otherwise be deadlocked.
*/
private Savepoint autoSavepoint;
private final int maxInternalRetries;
private final ReentrantLock abortedLock = new ReentrantLock();
private final long transactionId;
private final DatabaseClient dbClient;
private final TransactionOption[] transactionOptions;
private TransactionManager txManager;
private final boolean retryAbortsInternally;
private final boolean delayTransactionStartUntilFirstWrite;
private final SavepointSupport savepointSupport;
private int transactionRetryAttempts;
private int successfulRetries;
private final List transactionRetryListeners;
private volatile ApiFuture txContextFuture;
private boolean canUseSingleUseRead;
private volatile SettableApiFuture commitResponseFuture;
private volatile UnitOfWorkState state = UnitOfWorkState.STARTED;
private volatile AbortedException abortedException;
private AbortedException rolledBackToSavepointException;
private boolean timedOutOrCancelled = false;
private final List statements = new ArrayList<>();
private final List mutations = new ArrayList<>();
private Timestamp transactionStarted;
private static final class RollbackToSavepointException extends Exception {
private final Savepoint savepoint;
RollbackToSavepointException(Savepoint savepoint) {
this.savepoint = Preconditions.checkNotNull(savepoint);
}
Savepoint getSavepoint() {
return this.savepoint;
}
}
static class Builder extends AbstractMultiUseTransaction.Builder {
private boolean useAutoSavepointsForEmulator;
private DatabaseClient dbClient;
private Boolean retryAbortsInternally;
private boolean delayTransactionStartUntilFirstWrite;
private boolean returnCommitStats;
private Duration maxCommitDelay;
private SavepointSupport savepointSupport;
private List transactionRetryListeners;
private Builder() {}
Builder setUseAutoSavepointsForEmulator(boolean useAutoSavepoints) {
this.useAutoSavepointsForEmulator = useAutoSavepoints;
return this;
}
Builder setDatabaseClient(DatabaseClient client) {
Preconditions.checkNotNull(client);
this.dbClient = client;
return this;
}
Builder setDelayTransactionStartUntilFirstWrite(boolean delayTransactionStartUntilFirstWrite) {
this.delayTransactionStartUntilFirstWrite = delayTransactionStartUntilFirstWrite;
return this;
}
Builder setRetryAbortsInternally(boolean retryAbortsInternally) {
this.retryAbortsInternally = retryAbortsInternally;
return this;
}
Builder setReturnCommitStats(boolean returnCommitStats) {
this.returnCommitStats = returnCommitStats;
return this;
}
Builder setMaxCommitDelay(Duration maxCommitDelay) {
this.maxCommitDelay = maxCommitDelay;
return this;
}
Builder setSavepointSupport(SavepointSupport savepointSupport) {
this.savepointSupport = savepointSupport;
return this;
}
Builder setTransactionRetryListeners(List listeners) {
Preconditions.checkNotNull(listeners);
this.transactionRetryListeners = listeners;
return this;
}
@Override
ReadWriteTransaction build() {
Preconditions.checkState(dbClient != null, "No DatabaseClient client specified");
Preconditions.checkState(
retryAbortsInternally != null, "RetryAbortsInternally is not specified");
Preconditions.checkState(
transactionRetryListeners != null, "TransactionRetryListeners are not specified");
Preconditions.checkState(savepointSupport != null, "SavepointSupport is not specified");
return new ReadWriteTransaction(this);
}
}
static Builder newBuilder() {
return new Builder();
}
private ReadWriteTransaction(Builder builder) {
super(builder);
this.transactionId = ID_GENERATOR.incrementAndGet();
this.useAutoSavepointsForEmulator =
builder.useAutoSavepointsForEmulator && builder.retryAbortsInternally;
// Use a higher max for internal retries if auto-savepoints have been enabled for the emulator.
// This can cause a larger number of transactions to be aborted and retried, and retrying on the
// emulator is fast, so increasing the limit is reasonable.
this.maxInternalRetries =
this.useAutoSavepointsForEmulator
? DEFAULT_MAX_INTERNAL_RETRIES * 10
: DEFAULT_MAX_INTERNAL_RETRIES;
this.dbClient = builder.dbClient;
this.delayTransactionStartUntilFirstWrite = builder.delayTransactionStartUntilFirstWrite;
this.retryAbortsInternally = builder.retryAbortsInternally;
this.savepointSupport = builder.savepointSupport;
this.transactionRetryListeners = builder.transactionRetryListeners;
this.transactionOptions = extractOptions(builder);
}
private TransactionOption[] extractOptions(Builder builder) {
int numOptions = 0;
if (builder.returnCommitStats) {
numOptions++;
}
if (builder.maxCommitDelay != null) {
numOptions++;
}
if (this.transactionTag != null) {
numOptions++;
}
if (this.excludeTxnFromChangeStreams) {
numOptions++;
}
if (this.rpcPriority != null) {
numOptions++;
}
TransactionOption[] options = new TransactionOption[numOptions];
int index = 0;
if (builder.returnCommitStats) {
options[index++] = Options.commitStats();
}
if (builder.maxCommitDelay != null) {
options[index++] = Options.maxCommitDelay(builder.maxCommitDelay);
}
if (this.transactionTag != null) {
options[index++] = Options.tag(this.transactionTag);
}
if (this.excludeTxnFromChangeStreams) {
options[index++] = Options.excludeTxnFromChangeStreams();
}
if (this.rpcPriority != null) {
options[index++] = Options.priority(this.rpcPriority);
}
return options;
}
@Override
public String toString() {
return new StringBuilder()
.append("ReadWriteTransaction - ID: ")
.append(transactionId)
.append("; Delay tx start: ")
.append(delayTransactionStartUntilFirstWrite)
.append("; Tag: ")
.append(Strings.nullToEmpty(transactionTag))
.append("; Status: ")
.append(internalGetStateName())
.append("; Started: ")
.append(internalGetTimeStarted())
.append("; Retry attempts: ")
.append(transactionRetryAttempts)
.append("; Successful retries: ")
.append(successfulRetries)
.toString();
}
private String internalGetStateName() {
return transactionStarted == null ? "Not yet started" : getState().toString();
}
private String internalGetTimeStarted() {
return transactionStarted == null ? "Not yet started" : transactionStarted.toString();
}
@Override
public UnitOfWorkState getState() {
return this.state;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
void checkOrCreateValidTransaction(ParsedStatement statement, CallType callType) {
checkValidStateAndMarkStarted();
if (txContextFuture == null
&& (!delayTransactionStartUntilFirstWrite
|| (statement != null && statement.isUpdate())
|| (statement == COMMIT_STATEMENT && !mutations.isEmpty()))) {
txManager = dbClient.transactionManager(this.transactionOptions);
canUseSingleUseRead = false;
txContextFuture =
executeStatementAsync(
callType, BEGIN_STATEMENT, txManager::begin, SpannerGrpc.getBeginTransactionMethod());
} else if (txContextFuture == null && delayTransactionStartUntilFirstWrite) {
canUseSingleUseRead = true;
}
maybeUpdateActiveTransaction();
}
private void checkValidStateAndMarkStarted() {
ConnectionPreconditions.checkState(
this.state == UnitOfWorkState.STARTED || this.state == UnitOfWorkState.ABORTED,
"This transaction has status "
+ this.state.name()
+ ", only "
+ UnitOfWorkState.STARTED
+ "or "
+ UnitOfWorkState.ABORTED
+ " is allowed.");
ConnectionPreconditions.checkState(
this.retryAbortsInternally || this.rolledBackToSavepointException == null,
"Cannot resume execution after rolling back to a savepoint if internal retries have been disabled. "
+ "Call Connection#setRetryAbortsInternally(true) or execute `SET RETRY_ABORTS_INTERNALLY=TRUE` to enable "
+ "resuming execution after rolling back to a savepoint.");
checkTimedOut();
if (transactionStarted == null) {
transactionStarted = Timestamp.now();
}
}
private void checkTimedOut() {
ConnectionPreconditions.checkState(
!timedOutOrCancelled,
"The last statement of this transaction timed out or was cancelled. "
+ "The transaction is no longer usable. "
+ "Rollback the transaction and start a new one.");
}
@Override
public boolean isActive() {
// Consider ABORTED an active state, as it is something that is automatically set if the
// transaction is aborted by the backend. That means that we should not automatically create a
// new transaction for the following statement after a transaction has aborted, and instead we
// should wait until the application has rolled back the current transaction.
//
// Otherwise the following list of statements could show unexpected behavior:
// connection.executeUpdateAsync("UPDATE FOO SET BAR=1 ...");
// connection.executeUpdateAsync("UPDATE BAR SET FOO=2 ...");
// connection.commitAsync();
//
// If the first update statement fails with an aborted exception, the second update statement
// should not be executed in a new transaction, but should also abort.
return getState().isActive() || state == UnitOfWorkState.ABORTED;
}
void checkAborted() {
if (this.state == UnitOfWorkState.ABORTED && this.abortedException != null) {
if (this.abortedException instanceof AbortedDueToConcurrentModificationException) {
throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(
(AbortedDueToConcurrentModificationException) this.abortedException);
} else {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.ABORTED,
"This transaction has already been aborted. Rollback this transaction to start a new one.",
this.abortedException);
}
}
}
void checkRolledBackToSavepoint() {
if (this.rolledBackToSavepointException != null) {
if (savepointSupport == SavepointSupport.FAIL_AFTER_ROLLBACK
&& !((RollbackToSavepointException) this.rolledBackToSavepointException.getCause())
.getSavepoint()
.isAutoSavepoint()) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.FAILED_PRECONDITION,
"Using a read/write transaction after rolling back to a savepoint is not supported "
+ "with SavepointSupport="
+ savepointSupport);
} else {
AbortedException exception = this.rolledBackToSavepointException;
this.rolledBackToSavepointException = null;
throw exception;
}
}
}
@Override
ReadContext getReadContext() {
if (txContextFuture == null && canUseSingleUseRead) {
return dbClient.singleUse();
}
ConnectionPreconditions.checkState(txContextFuture != null, "Missing transaction context");
return get(txContextFuture);
}
TransactionContext getTransactionContext() {
ConnectionPreconditions.checkState(txContextFuture != null, "Missing transaction context");
return (TransactionContext) getReadContext();
}
@Override
public Timestamp getReadTimestamp() {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.FAILED_PRECONDITION,
"There is no read timestamp available for read/write transactions.");
}
@Override
public Timestamp getReadTimestampOrNull() {
return null;
}
private boolean hasCommitResponse() {
return commitResponseFuture != null;
}
@Override
public Timestamp getCommitTimestamp() {
ConnectionPreconditions.checkState(
hasCommitResponse(), "This transaction has not been committed.");
return get(commitResponseFuture).getCommitTimestamp();
}
@Override
public Timestamp getCommitTimestampOrNull() {
return hasCommitResponse() ? get(commitResponseFuture).getCommitTimestamp() : null;
}
@Override
public CommitResponse getCommitResponse() {
ConnectionPreconditions.checkState(
hasCommitResponse(), "This transaction has not been committed.");
return get(commitResponseFuture);
}
@Override
public CommitResponse getCommitResponseOrNull() {
return hasCommitResponse() ? get(commitResponseFuture) : null;
}
@Override
public ApiFuture executeDdlAsync(CallType callType, ParsedStatement ddl) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.FAILED_PRECONDITION,
"DDL-statements are not allowed inside a read/write transaction.");
}
private void handlePossibleInvalidatingException(SpannerException e) {
if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED
|| e.getErrorCode() == ErrorCode.CANCELLED) {
this.timedOutOrCancelled = true;
}
}
@Override
public ApiFuture executeQueryAsync(
final CallType callType,
final ParsedStatement statement,
final AnalyzeMode analyzeMode,
final QueryOption... options) {
Preconditions.checkArgument(
(statement.getType() == StatementType.QUERY)
|| (statement.getType() == StatementType.UPDATE && statement.hasReturningClause()),
"Statement must be a query or DML with returning clause");
try (Scope ignore = span.makeCurrent()) {
checkOrCreateValidTransaction(statement, callType);
ApiFuture res;
if (retryAbortsInternally && txContextFuture != null) {
res =
executeStatementAsync(
callType,
statement,
() -> {
checkTimedOut();
return runWithRetry(
() -> {
try {
getStatementExecutor()
.invokeInterceptors(
statement,
StatementExecutionStep.EXECUTE_STATEMENT,
ReadWriteTransaction.this);
DirectExecuteResultSet delegate =
DirectExecuteResultSet.ofResultSet(
internalExecuteQuery(statement, analyzeMode, options));
return createAndAddRetryResultSet(
delegate, statement, analyzeMode, options);
} catch (AbortedException e) {
throw e;
} catch (SpannerException e) {
createAndAddFailedQuery(e, statement, analyzeMode, options);
throw e;
}
});
},
// ignore interceptors here as they are invoked in the Callable.
InterceptorsUsage.IGNORE_INTERCEPTORS,
ImmutableList.of(SpannerGrpc.getExecuteStreamingSqlMethod()));
} else {
res = super.executeQueryAsync(callType, statement, analyzeMode, options);
}
ApiFutures.addCallback(
res,
new ApiFutureCallback() {
@Override
public void onFailure(Throwable t) {
if (t instanceof SpannerException) {
handlePossibleInvalidatingException((SpannerException) t);
}
}
@Override
public void onSuccess(ResultSet result) {}
},
MoreExecutors.directExecutor());
return res;
}
}
@Override
public ApiFuture analyzeUpdateAsync(
CallType callType, ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
try (Scope ignore = span.makeCurrent()) {
return ApiFutures.transform(
internalExecuteUpdateAsync(callType, update, analyzeMode, options),
Tuple::y,
MoreExecutors.directExecutor());
}
}
@Override
public ApiFuture executeUpdateAsync(
CallType callType, final ParsedStatement update, final UpdateOption... options) {
try (Scope ignore = span.makeCurrent()) {
return ApiFutures.transform(
internalExecuteUpdateAsync(callType, update, AnalyzeMode.NONE, options),
Tuple::x,
MoreExecutors.directExecutor());
}
}
/**
* Executes the given update statement using the specified query planning mode and with the given
* options and returns the result as a {@link Tuple}. The tuple contains either a {@link
* ResultSet} with the query plan and execution statistics, or a {@link Long} that contains the
* update count that was returned for the update statement. Only one of the elements in the tuple
* will be set, and the reason that we are using a {@link Tuple} here is because Java does not
* have a standard implementation for an 'Either' class (i.e. a Tuple where only one element is
* set). An alternative would be to always return a {@link ResultSet} with the update count
* encoded in the execution stats of the result set, but this would mean that we would create
* additional {@link ResultSet} instances every time an update statement is executed in normal
* mode.
*/
private ApiFuture> internalExecuteUpdateAsync(
CallType callType, ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
Preconditions.checkNotNull(update);
Preconditions.checkArgument(update.isUpdate(), "The statement is not an update statement");
checkOrCreateValidTransaction(update, callType);
ApiFuture> res;
if (retryAbortsInternally && txContextFuture != null) {
res =
executeStatementAsync(
callType,
update,
() -> {
checkTimedOut();
return runWithRetry(
() -> {
try {
getStatementExecutor()
.invokeInterceptors(
update,
StatementExecutionStep.EXECUTE_STATEMENT,
ReadWriteTransaction.this);
Tuple result;
long updateCount;
if (analyzeMode == AnalyzeMode.NONE) {
updateCount =
get(txContextFuture).executeUpdate(update.getStatement(), options);
result = Tuple.of(updateCount, null);
} else {
ResultSet resultSet =
get(txContextFuture)
.analyzeUpdateStatement(
update.getStatement(),
analyzeMode.getQueryAnalyzeMode(),
options);
updateCount =
Objects.requireNonNull(resultSet.getStats()).getRowCountExact();
result = Tuple.of(null, resultSet);
}
createAndAddRetriableUpdate(update, analyzeMode, updateCount, options);
return result;
} catch (AbortedException e) {
throw e;
} catch (SpannerException e) {
createAndAddFailedUpdate(e, update);
throw e;
}
});
},
// ignore interceptors here as they are invoked in the Callable.
InterceptorsUsage.IGNORE_INTERCEPTORS,
ImmutableList.of(SpannerGrpc.getExecuteSqlMethod()));
} else {
res =
executeStatementAsync(
callType,
update,
() -> {
checkTimedOut();
checkAborted();
if (analyzeMode == AnalyzeMode.NONE) {
return Tuple.of(
get(txContextFuture).executeUpdate(update.getStatement(), options), null);
}
ResultSet resultSet =
get(txContextFuture)
.analyzeUpdateStatement(
update.getStatement(), analyzeMode.getQueryAnalyzeMode(), options);
return Tuple.of(null, resultSet);
},
SpannerGrpc.getExecuteSqlMethod());
}
ApiFutures.addCallback(
res,
new ApiFutureCallback>() {
@Override
public void onFailure(Throwable t) {
if (t instanceof SpannerException) {
handlePossibleInvalidatingException((SpannerException) t);
}
}
@Override
public void onSuccess(Tuple result) {}
},
MoreExecutors.directExecutor());
return res;
}
@Override
public ApiFuture executeBatchUpdateAsync(
CallType callType, Iterable updates, final UpdateOption... options) {
Preconditions.checkNotNull(updates);
try (Scope ignore = span.makeCurrent()) {
final List updateStatements = new LinkedList<>();
for (ParsedStatement update : updates) {
Preconditions.checkArgument(
update.isUpdate(),
"Statement is not an update statement: " + update.getSqlWithoutComments());
updateStatements.add(update.getStatement());
}
checkOrCreateValidTransaction(Iterables.getFirst(updates, null), callType);
ApiFuture res;
if (retryAbortsInternally) {
res =
executeStatementAsync(
callType,
RUN_BATCH_STATEMENT,
() -> {
checkTimedOut();
return runWithRetry(
() -> {
try {
getStatementExecutor()
.invokeInterceptors(
RUN_BATCH_STATEMENT,
StatementExecutionStep.EXECUTE_STATEMENT,
ReadWriteTransaction.this);
long[] updateCounts =
get(txContextFuture).batchUpdate(updateStatements, options);
createAndAddRetriableBatchUpdate(updateStatements, updateCounts, options);
return updateCounts;
} catch (AbortedException e) {
throw e;
} catch (SpannerException e) {
createAndAddFailedBatchUpdate(e, updateStatements);
throw e;
}
});
},
// ignore interceptors here as they are invoked in the Callable.
InterceptorsUsage.IGNORE_INTERCEPTORS,
ImmutableList.of(SpannerGrpc.getExecuteBatchDmlMethod()));
} else {
res =
executeStatementAsync(
callType,
RUN_BATCH_STATEMENT,
() -> {
checkTimedOut();
checkAborted();
return get(txContextFuture).batchUpdate(updateStatements);
},
SpannerGrpc.getExecuteBatchDmlMethod());
}
ApiFutures.addCallback(
res,
new ApiFutureCallback() {
@Override
public void onFailure(Throwable t) {
if (t instanceof SpannerException) {
handlePossibleInvalidatingException((SpannerException) t);
}
}
@Override
public void onSuccess(long[] result) {}
},
MoreExecutors.directExecutor());
return res;
}
}
@Override
public ApiFuture writeAsync(CallType callType, Iterable mutations) {
try (Scope ignore = span.makeCurrent()) {
Preconditions.checkNotNull(mutations);
// We actually don't need an underlying transaction yet, as mutations are buffered until
// commit.
// But we do need to verify that this transaction is valid, and to mark the start of the
// transaction.
checkValidStateAndMarkStarted();
for (Mutation mutation : mutations) {
this.mutations.add(checkNotNull(mutation));
}
return ApiFutures.immediateFuture(null);
}
}
private final Callable commitCallable =
new Callable() {
@Override
public Void call() {
checkAborted();
get(txContextFuture).buffer(mutations);
txManager.commit();
commitResponseFuture.set(txManager.getCommitResponse());
state = UnitOfWorkState.COMMITTED;
return null;
}
};
@Override
public ApiFuture commitAsync(CallType callType) {
try (Scope ignore = span.makeCurrent()) {
checkOrCreateValidTransaction(COMMIT_STATEMENT, callType);
state = UnitOfWorkState.COMMITTING;
commitResponseFuture = SettableApiFuture.create();
ApiFuture res;
// Check if this transaction actually needs to commit anything.
if (txContextFuture == null) {
// No actual transaction was started by this read/write transaction, which also means that
// we
// don't have to commit anything.
commitResponseFuture.set(
new CommitResponse(
Timestamp.fromProto(com.google.protobuf.Timestamp.getDefaultInstance())));
state = UnitOfWorkState.COMMITTED;
res = SettableApiFuture.create();
((SettableApiFuture) res).set(null);
} else if (retryAbortsInternally) {
res =
executeStatementAsync(
callType,
COMMIT_STATEMENT,
() -> {
checkTimedOut();
try {
return runWithRetry(
() -> {
getStatementExecutor()
.invokeInterceptors(
COMMIT_STATEMENT,
StatementExecutionStep.EXECUTE_STATEMENT,
ReadWriteTransaction.this);
return commitCallable.call();
});
} catch (Throwable t) {
commitResponseFuture.setException(t);
state = UnitOfWorkState.COMMIT_FAILED;
try {
txManager.close();
} catch (Throwable t2) {
// Ignore.
}
throw t;
}
},
InterceptorsUsage.IGNORE_INTERCEPTORS,
ImmutableList.of(SpannerGrpc.getCommitMethod()));
} else {
res =
executeStatementAsync(
callType,
COMMIT_STATEMENT,
() -> {
checkTimedOut();
try {
return commitCallable.call();
} catch (Throwable t) {
commitResponseFuture.setException(t);
state = UnitOfWorkState.COMMIT_FAILED;
try {
txManager.close();
} catch (Throwable t2) {
// Ignore.
}
throw t;
}
},
SpannerGrpc.getCommitMethod());
}
asyncEndUnitOfWorkSpan();
return res;
}
}
/**
* Executes a database call that could throw an {@link AbortedException}. If an {@link
* AbortedException} is thrown, the transaction will automatically be retried and the checksums of
* all {@link ResultSet}s and update counts of DML statements will be checked against the original
* values of the original transaction. If the checksums and/or update counts do not match, the
* method will throw an {@link AbortedException} that cannot be retried, as the underlying data
* have actually changed.
*
* If {@link ReadWriteTransaction#retryAbortsInternally} has been set to false
,
* this method will throw an exception instead of retrying the transaction if the transaction was
* aborted.
*
* @param callable The actual database calls.
* @return the results of the database calls.
* @throws SpannerException if the database calls threw an exception, an {@link
* AbortedDueToConcurrentModificationException} if a retry of the transaction yielded
* different results than the original transaction, or an {@link AbortedException} if the
* maximum number of retries has been exceeded.
*/
T runWithRetry(Callable callable) throws SpannerException {
while (true) {
abortedLock.lock();
try {
checkAborted();
try {
checkRolledBackToSavepoint();
T result = callable.call();
if (this.useAutoSavepointsForEmulator) {
this.autoSavepoint = createAutoSavepoint();
}
return result;
} catch (final AbortedException aborted) {
handleAborted(aborted);
} catch (SpannerException e) {
throw e;
} catch (Exception e) {
throw SpannerExceptionFactory.asSpannerException(e);
}
} finally {
abortedLock.unlock();
}
}
}
private void maybeUpdateActiveTransaction() {
if (this.useAutoSavepointsForEmulator) {
if (CURRENT_ACTIVE_TRANSACTION.get() != null && CURRENT_ACTIVE_TRANSACTION.get() != this) {
ReadWriteTransaction activeTransaction = CURRENT_ACTIVE_TRANSACTION.get();
if (activeTransaction.isActive() && activeTransaction.autoSavepoint != null) {
activeTransaction.rollbackToSavepoint(activeTransaction.autoSavepoint);
activeTransaction.autoSavepoint = null;
}
CURRENT_ACTIVE_TRANSACTION.remove();
}
CURRENT_ACTIVE_TRANSACTION.set(this);
}
}
/**
* Registers a {@link ResultSet} on this transaction that must be checked during a retry, and
* returns a retryable {@link ResultSet}.
*/
private ResultSet createAndAddRetryResultSet(
ProtobufResultSet resultSet,
ParsedStatement statement,
AnalyzeMode analyzeMode,
QueryOption... options) {
if (retryAbortsInternally) {
ChecksumResultSet checksumResultSet =
createChecksumResultSet(resultSet, statement, analyzeMode, options);
addRetryStatement(checksumResultSet);
return checksumResultSet;
}
return resultSet;
}
/** Registers the statement as a query that should return an error during a retry. */
private void createAndAddFailedQuery(
SpannerException e,
ParsedStatement statement,
AnalyzeMode analyzeMode,
QueryOption... options) {
if (retryAbortsInternally) {
addRetryStatement(new FailedQuery(this, e, statement, analyzeMode, options));
}
}
private void createAndAddRetriableUpdate(
ParsedStatement update, AnalyzeMode analyzeMode, long updateCount, UpdateOption... options) {
if (retryAbortsInternally) {
addRetryStatement(new RetriableUpdate(this, update, analyzeMode, updateCount, options));
}
}
private void createAndAddRetriableBatchUpdate(
Iterable updates, long[] updateCounts, UpdateOption... options) {
if (retryAbortsInternally) {
addRetryStatement(new RetriableBatchUpdate(this, updates, updateCounts, options));
}
}
/** Registers the statement as an update that should return an error during a retry. */
private void createAndAddFailedUpdate(SpannerException e, ParsedStatement update) {
if (retryAbortsInternally) {
addRetryStatement(new FailedUpdate(this, e, update));
}
}
/** Registers the statements as a batch of updates that should return an error during a retry. */
private void createAndAddFailedBatchUpdate(SpannerException e, Iterable updates) {
if (retryAbortsInternally) {
addRetryStatement(new FailedBatchUpdate(this, e, updates));
}
}
/**
* Adds a statement to the list of statements that should be retried if this transaction aborts.
*/
private void addRetryStatement(RetriableStatement statement) {
Preconditions.checkState(
retryAbortsInternally, "retryAbortsInternally is not enabled for this transaction");
statements.add(statement);
}
/**
* Handles an aborted exception by checking whether the transaction may be retried internally, and
* if so, does the retry. If retry is not allowed, or if the retry fails, the method will throw an
* {@link AbortedException}.
*/
private void handleAborted(AbortedException aborted) {
if (transactionRetryAttempts >= maxInternalRetries) {
// If the same statement in transaction keeps aborting, then we need to abort here.
span.addEvent("Internal retry attempts exceeded");
throwAbortWithRetryAttemptsExceeded();
} else if (retryAbortsInternally) {
logger.fine(toString() + ": Starting internal transaction retry");
while (true) {
// First back off and then restart the transaction.
long delay = aborted.getRetryDelayInMillis();
span.addEvent(
"Transaction aborted. Backing off for " + delay + " milliseconds and retrying.");
try {
if (delay > 0L) {
//noinspection BusyWait
Thread.sleep(delay);
} else if (aborted.isEmulatorOnlySupportsOneTransactionException()) {
//noinspection BusyWait
Thread.sleep(ThreadLocalRandom.current().nextInt(50));
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.CANCELLED, "The statement was cancelled");
}
try {
if (aborted.getCause() instanceof RollbackToSavepointException) {
txManager = dbClient.transactionManager(transactionOptions);
txContextFuture = ApiFutures.immediateFuture(txManager.begin());
} else {
txContextFuture = ApiFutures.immediateFuture(txManager.resetForRetry());
}
// Inform listeners about the transaction retry that is about to start.
invokeTransactionRetryListenersOnStart();
// Then retry all transaction statements.
transactionRetryAttempts++;
for (RetriableStatement statement : statements) {
statement.retry(aborted);
}
successfulRetries++;
invokeTransactionRetryListenersOnFinish(RetryResult.RETRY_SUCCESSFUL);
logger.fine(
toString()
+ ": Internal transaction retry succeeded. Starting retry of original statement.");
// Retry succeeded, return and continue the original transaction.
break;
} catch (AbortedDueToConcurrentModificationException e) {
// Retry failed because of a concurrent modification, we have to abort.
invokeTransactionRetryListenersOnFinish(
RetryResult.RETRY_ABORTED_DUE_TO_CONCURRENT_MODIFICATION);
logger.fine(
toString() + ": Internal transaction retry aborted due to a concurrent modification");
// Do a shoot and forget rollback.
try {
txManager.rollback();
} catch (Throwable t) {
// ignore
}
this.state = UnitOfWorkState.ABORTED;
this.abortedException = e;
throw e;
} catch (AbortedException abortedExceptionDuringRetry) {
// Retry aborted, do another retry of the transaction.
if (transactionRetryAttempts >= maxInternalRetries) {
throwAbortWithRetryAttemptsExceeded();
}
invokeTransactionRetryListenersOnFinish(RetryResult.RETRY_ABORTED_AND_RESTARTING);
logger.fine(toString() + ": Internal transaction retry aborted, trying again");
// Use the new aborted exception to determine both the backoff delay and how to handle
// the retry.
aborted = abortedExceptionDuringRetry;
} catch (SpannerException e) {
// unexpected exception
logger.log(
Level.FINE,
toString() + ": Internal transaction retry failed due to an unexpected exception",
e);
// Do a shoot and forget rollback.
try {
txManager.rollback();
} catch (Throwable t) {
// ignore
}
// Set transaction state to aborted as the retry failed.
this.state = UnitOfWorkState.ABORTED;
this.abortedException = aborted;
// Re-throw underlying exception.
throw e;
}
}
} else {
try {
txManager.close();
} catch (Throwable t) {
// ignore
}
// Internal retry is not enabled.
this.state = UnitOfWorkState.ABORTED;
this.abortedException = aborted;
throw aborted;
}
}
private void throwAbortWithRetryAttemptsExceeded() throws SpannerException {
invokeTransactionRetryListenersOnFinish(RetryResult.RETRY_ABORTED_AND_MAX_ATTEMPTS_EXCEEDED);
logger.fine(
toString()
+ ": Internal transaction retry aborted and max number of retry attempts has been exceeded");
// Try to rollback the transaction and ignore any exceptions.
// Normally it should not be necessary to do this, but in order to be sure we never leak
// any sessions it is better to do so.
try {
txManager.rollback();
} catch (Throwable t) {
// ignore
}
this.state = UnitOfWorkState.ABORTED;
this.abortedException =
(AbortedException)
SpannerExceptionFactory.newSpannerException(
ErrorCode.ABORTED, MAX_INTERNAL_RETRIES_EXCEEDED);
throw this.abortedException;
}
private void invokeTransactionRetryListenersOnStart() {
for (TransactionRetryListener listener : transactionRetryListeners) {
listener.retryStarting(transactionStarted, transactionId, transactionRetryAttempts);
}
}
private void invokeTransactionRetryListenersOnFinish(RetryResult result) {
for (TransactionRetryListener listener : transactionRetryListeners) {
listener.retryFinished(transactionStarted, transactionId, transactionRetryAttempts, result);
}
}
private final Callable rollbackCallable =
new Callable() {
@Override
public Void call() {
try {
if (state != UnitOfWorkState.ABORTED && rolledBackToSavepointException == null) {
// Make sure the transaction has actually started before we try to rollback.
get(txContextFuture);
txManager.rollback();
}
return null;
} finally {
txManager.close();
}
}
};
@Override
public ApiFuture rollbackAsync(CallType callType) {
try (Scope ignore = span.makeCurrent()) {
return rollbackAsync(callType, true);
}
}
private ApiFuture rollbackAsync(CallType callType, boolean updateStatus) {
ConnectionPreconditions.checkState(
state == UnitOfWorkState.STARTED || state == UnitOfWorkState.ABORTED,
"This transaction has status " + state.name());
if (updateStatus) {
state = UnitOfWorkState.ROLLED_BACK;
asyncEndUnitOfWorkSpan();
}
if (txContextFuture != null && state != UnitOfWorkState.ABORTED) {
ApiFuture result =
executeStatementAsync(
callType, ROLLBACK_STATEMENT, rollbackCallable, SpannerGrpc.getRollbackMethod());
asyncEndUnitOfWorkSpan();
return result;
} else {
return asyncEndUnitOfWorkSpan();
}
}
@Override
String getUnitOfWorkName() {
return "read/write transaction";
}
static class ReadWriteSavepoint extends Savepoint {
private final int statementPosition;
private final int mutationPosition;
ReadWriteSavepoint(String name, int statementPosition, int mutationPosition) {
this(name, statementPosition, mutationPosition, false);
}
ReadWriteSavepoint(
String name, int statementPosition, int mutationPosition, boolean autoSavepoint) {
super(name, autoSavepoint);
this.statementPosition = statementPosition;
this.mutationPosition = mutationPosition;
}
@Override
int getStatementPosition() {
return this.statementPosition;
}
@Override
int getMutationPosition() {
return this.mutationPosition;
}
}
@Override
Savepoint savepoint(String name) {
return new ReadWriteSavepoint(name, statements.size(), mutations.size());
}
private Savepoint createAutoSavepoint() {
return new ReadWriteSavepoint(AUTO_SAVEPOINT_NAME, statements.size(), mutations.size(), true);
}
@Override
void rollbackToSavepoint(Savepoint savepoint) {
try (Scope ignore = span.makeCurrent()) {
get(rollbackAsync(CallType.SYNC, false));
// Mark the state of the transaction as rolled back to a savepoint. This will ensure that the
// transaction will retry the next time a statement is actually executed.
this.rolledBackToSavepointException =
(AbortedException)
SpannerExceptionFactory.newSpannerException(
ErrorCode.ABORTED,
"Transaction has been rolled back to a savepoint",
new RollbackToSavepointException(savepoint));
// Clear all statements and mutations after the savepoint.
this.statements.subList(savepoint.getStatementPosition(), this.statements.size()).clear();
this.mutations.subList(savepoint.getMutationPosition(), this.mutations.size()).clear();
}
}
/**
* A retriable statement is a query or DML statement during a read/write transaction that can be
* retried if the original transaction aborted.
*/
interface RetriableStatement {
/**
* Retry this statement in a new transaction. Throws an {@link
* AbortedDueToConcurrentModificationException} if the retry could not successfully be executed
* because of an actual concurrent modification of the underlying data. This {@link
* AbortedDueToConcurrentModificationException} cannot be retried.
*/
void retry(AbortedException aborted) throws AbortedException;
}
/** Creates a {@link ChecksumResultSet} for this {@link ReadWriteTransaction}. */
@VisibleForTesting
ChecksumResultSet createChecksumResultSet(
ProtobufResultSet delegate,
ParsedStatement statement,
AnalyzeMode analyzeMode,
QueryOption... options) {
return new ChecksumResultSet(this, delegate, statement, analyzeMode, options);
}
}